diff --git a/StabilityMatrix.Avalonia/Services/AccountsService.cs b/StabilityMatrix.Avalonia/Services/AccountsService.cs index a28ff15f..50dfa97d 100644 --- a/StabilityMatrix.Avalonia/Services/AccountsService.cs +++ b/StabilityMatrix.Avalonia/Services/AccountsService.cs @@ -40,10 +40,25 @@ public class AccountsService : IAccountsService { var secrets = GlobalUserSecrets.LoadFromFile(); - var loginResponse = await lykosAuthApi.PostLogin(new PostLoginRequest(email, password)); + var tokens = await lykosAuthApi.PostLogin(new PostLoginRequest(email, password)); - secrets.LykosAccessToken = loginResponse.AccessToken; - secrets.LykosRefreshToken = loginResponse.RefreshToken; + secrets.LykosAccessToken = tokens.AccessToken; + secrets.LykosRefreshToken = tokens.RefreshToken; + secrets.SaveToFile(); + + await RefreshAsync(); + } + + public async Task LykosSignupAsync(string email, string password, string username) + { + var secrets = GlobalUserSecrets.LoadFromFile(); + + var tokens = await lykosAuthApi.PostAccount( + new PostAccountRequest(email, password, password, username) + ); + + secrets.LykosAccessToken = tokens.AccessToken; + secrets.LykosRefreshToken = tokens.RefreshToken; secrets.SaveToFile(); await RefreshAsync(); @@ -75,7 +90,7 @@ public class AccountsService : IAccountsService { try { - var user = await lykosAuthApi.GetUser("dev@ionite.io"); + var user = await lykosAuthApi.GetUserSelf(); OnLykosAccountStatusUpdate( new LykosAccountStatusUpdateEventArgs { IsConnected = true, User = user } diff --git a/StabilityMatrix.Avalonia/Services/IAccountsService.cs b/StabilityMatrix.Avalonia/Services/IAccountsService.cs index 6f3dfc99..59cdc98c 100644 --- a/StabilityMatrix.Avalonia/Services/IAccountsService.cs +++ b/StabilityMatrix.Avalonia/Services/IAccountsService.cs @@ -10,6 +10,8 @@ public interface IAccountsService LykosAccountStatusUpdateEventArgs? LykosStatus { get; } + Task LykosSignupAsync(string email, string password, string username); + Task LykosLoginAsync(string email, string password); Task LykosLogoutAsync(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs index 116620e0..3b50d45b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs @@ -5,11 +5,13 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using Refit; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Validators; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -44,6 +46,12 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase [Required, RequiresMatch(nameof(Password))] private string? confirmPassword; + [ObservableProperty] + private AppException? loginError; + + [ObservableProperty] + private AppException? signupError; + public LykosLoginViewModel(IAccountsService accountsService) { this.accountsService = accountsService; @@ -55,7 +63,12 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase } [RelayCommand(CanExecute = nameof(CanExecuteContinueButtonClick))] - private async Task OnContinueButtonClick() + private Task OnContinueButtonClick() + { + return IsSignupMode ? SignupAsync() : LoginAsync(); + } + + private async Task LoginAsync() { try { @@ -63,9 +76,31 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase CloseDialog(TaskDialogStandardResult.OK); } - catch (Exception e) + catch (OperationCanceledException) + { + LoginError = new AppException("Request timed out", "Please try again later"); + } + catch (ApiException e) + { + LoginError = new AppException("Failed to login", $"{e.StatusCode} - {e.Message}"); + } + } + + private async Task SignupAsync() + { + try + { + await accountsService.LykosSignupAsync(Email!, Password!, Username!); + + CloseDialog(TaskDialogStandardResult.OK); + } + catch (OperationCanceledException) + { + SignupError = new AppException("Request timed out", "Please try again later"); + } + catch (ApiException e) { - Console.WriteLine(e); + SignupError = new AppException("Failed to signup", $"{e.StatusCode} - {e.Message}"); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs index 27dfba1e..8dc8e196 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -38,8 +41,25 @@ public partial class AccountSettingsViewModel : PageViewModelBase private bool isLykosConnected; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(LykosProfileImageUrl))] private GetUserResponse? lykosUser; + [ObservableProperty] + private string? lykosProfileImageUrl; + + partial void OnLykosUserChanged(GetUserResponse? value) + { + if (value?.Id is { } userEmail) + { + userEmail = userEmail.Trim().ToLowerInvariant(); + + var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(userEmail)); + var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + LykosProfileImageUrl = $"https://gravatar.com/avatar/{hash}?s=512&d=retro"; + } + } + public AccountSettingsViewModel( IAccountsService accountsService, ServiceManager vmFactory diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml index 6dc6003e..1491d202 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml @@ -29,7 +29,7 @@ + + + Grid.Row="3"/> @@ -58,7 +65,7 @@ + + diff --git a/StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml index a757495c..bc5b8718 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml @@ -68,22 +68,24 @@ - + CornerRadius="8" /> @@ -99,6 +101,7 @@ + diff --git a/StabilityMatrix.Core/Api/ILykosAuthApi.cs b/StabilityMatrix.Core/Api/ILykosAuthApi.cs index 24b68e09..f3a9765e 100644 --- a/StabilityMatrix.Core/Api/ILykosAuthApi.cs +++ b/StabilityMatrix.Core/Api/ILykosAuthApi.cs @@ -10,8 +10,18 @@ public interface ILykosAuthApi [Get("/api/Users/{email}")] Task GetUser(string email, CancellationToken cancellationToken = default); + [Headers("Authorization: Bearer")] + [Get("/api/Users/me")] + Task GetUserSelf(CancellationToken cancellationToken = default); + + [Post("/api/Accounts")] + Task PostAccount( + [Body] PostAccountRequest request, + CancellationToken cancellationToken = default + ); + [Post("/api/Login")] - Task PostLogin( + Task PostLogin( [Body] PostLoginRequest request, CancellationToken cancellationToken = default ); diff --git a/StabilityMatrix.Core/Exceptions/AppException.cs b/StabilityMatrix.Core/Exceptions/AppException.cs index 8c3b1f9f..51b73694 100644 --- a/StabilityMatrix.Core/Exceptions/AppException.cs +++ b/StabilityMatrix.Core/Exceptions/AppException.cs @@ -5,5 +5,17 @@ namespace StabilityMatrix.Core.Exceptions; /// public class AppException : ApplicationException { + public override string Message { get; } public string? Details { get; init; } + + public AppException(string message) + { + Message = message; + } + + public AppException(string message, string details) + { + Message = message; + Details = details; + } } diff --git a/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs b/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs index 9aa4cab0..f79a8ae7 100644 --- a/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs +++ b/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs @@ -2,7 +2,7 @@ public record GetUserResponse( string Id, - string AccountId, + LykosAccount Account, int UserLevel, string PatreonId, bool IsEmailVerified diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs new file mode 100644 index 00000000..6ecd7bbc --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs @@ -0,0 +1,3 @@ +namespace StabilityMatrix.Core.Models.Api.Lykos; + +public record LykosAccount(string Id, string Name); diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs new file mode 100644 index 00000000..193a388f --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs @@ -0,0 +1,3 @@ +namespace StabilityMatrix.Core.Models.Api.Lykos; + +public record LykosAccountTokens(string AccessToken, string RefreshToken); diff --git a/StabilityMatrix.Core/Models/Api/Lykos/PostAccountRequest.cs b/StabilityMatrix.Core/Models/Api/Lykos/PostAccountRequest.cs new file mode 100644 index 00000000..b71e28ca --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/PostAccountRequest.cs @@ -0,0 +1,8 @@ +namespace StabilityMatrix.Core.Models.Api.Lykos; + +public record PostAccountRequest( + string Email, + string Password, + string ConfirmPassword, + string AccountName +); diff --git a/StabilityMatrix.Core/Models/Api/Lykos/PostLoginResponse.cs b/StabilityMatrix.Core/Models/Api/Lykos/PostLoginResponse.cs deleted file mode 100644 index 60bd758c..00000000 --- a/StabilityMatrix.Core/Models/Api/Lykos/PostLoginResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace StabilityMatrix.Core.Models.Api.Lykos; - -public record PostLoginResponse(string AccessToken, string RefreshToken);