Browse Source

Add lykos signup and username profile image display

pull/324/head
Ionite 1 year ago
parent
commit
f61b1c8da2
No known key found for this signature in database
  1. 23
      StabilityMatrix.Avalonia/Services/AccountsService.cs
  2. 2
      StabilityMatrix.Avalonia/Services/IAccountsService.cs
  3. 41
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs
  4. 22
      StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs
  5. 20
      StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml
  6. 11
      StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml
  7. 12
      StabilityMatrix.Core/Api/ILykosAuthApi.cs
  8. 12
      StabilityMatrix.Core/Exceptions/AppException.cs
  9. 2
      StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs
  10. 3
      StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs
  11. 3
      StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs
  12. 8
      StabilityMatrix.Core/Models/Api/Lykos/PostAccountRequest.cs
  13. 3
      StabilityMatrix.Core/Models/Api/Lykos/PostLoginResponse.cs

23
StabilityMatrix.Avalonia/Services/AccountsService.cs

@ -40,10 +40,25 @@ public class AccountsService : IAccountsService
{ {
var secrets = GlobalUserSecrets.LoadFromFile(); 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.LykosAccessToken = tokens.AccessToken;
secrets.LykosRefreshToken = loginResponse.RefreshToken; 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(); secrets.SaveToFile();
await RefreshAsync(); await RefreshAsync();
@ -75,7 +90,7 @@ public class AccountsService : IAccountsService
{ {
try try
{ {
var user = await lykosAuthApi.GetUser("dev@ionite.io"); var user = await lykosAuthApi.GetUserSelf();
OnLykosAccountStatusUpdate( OnLykosAccountStatusUpdate(
new LykosAccountStatusUpdateEventArgs { IsConnected = true, User = user } new LykosAccountStatusUpdateEventArgs { IsConnected = true, User = user }

2
StabilityMatrix.Avalonia/Services/IAccountsService.cs

@ -10,6 +10,8 @@ public interface IAccountsService
LykosAccountStatusUpdateEventArgs? LykosStatus { get; } LykosAccountStatusUpdateEventArgs? LykosStatus { get; }
Task LykosSignupAsync(string email, string password, string username);
Task LykosLoginAsync(string email, string password); Task LykosLoginAsync(string email, string password);
Task LykosLogoutAsync(); Task LykosLogoutAsync();

41
StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs

@ -5,11 +5,13 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Refit;
using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Validators; using StabilityMatrix.Core.Validators;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -44,6 +46,12 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase
[Required, RequiresMatch<string>(nameof(Password))] [Required, RequiresMatch<string>(nameof(Password))]
private string? confirmPassword; private string? confirmPassword;
[ObservableProperty]
private AppException? loginError;
[ObservableProperty]
private AppException? signupError;
public LykosLoginViewModel(IAccountsService accountsService) public LykosLoginViewModel(IAccountsService accountsService)
{ {
this.accountsService = accountsService; this.accountsService = accountsService;
@ -55,7 +63,12 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase
} }
[RelayCommand(CanExecute = nameof(CanExecuteContinueButtonClick))] [RelayCommand(CanExecute = nameof(CanExecuteContinueButtonClick))]
private async Task OnContinueButtonClick() private Task OnContinueButtonClick()
{
return IsSignupMode ? SignupAsync() : LoginAsync();
}
private async Task LoginAsync()
{ {
try try
{ {
@ -63,9 +76,31 @@ public partial class LykosLoginViewModel : TaskDialogViewModelBase
CloseDialog(TaskDialogStandardResult.OK); 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}");
} }
} }

22
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 Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -38,8 +41,25 @@ public partial class AccountSettingsViewModel : PageViewModelBase
private bool isLykosConnected; private bool isLykosConnected;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(LykosProfileImageUrl))]
private GetUserResponse? lykosUser; 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( public AccountSettingsViewModel(
IAccountsService accountsService, IAccountsService accountsService,
ServiceManager<ViewModelBase> vmFactory ServiceManager<ViewModelBase> vmFactory

20
StabilityMatrix.Avalonia/Views/Dialogs/LykosLoginDialog.axaml

@ -29,7 +29,7 @@
<sg:SpacedGrid <sg:SpacedGrid
Margin="-4,32,-4,8" Margin="-4,32,-4,8"
RowDefinitions="Auto,Auto,*" RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="8"> RowSpacing="8">
<!-- Username --> <!-- Username -->
<TextBox <TextBox
@ -44,11 +44,18 @@
Watermark="{x:Static lang:Resources.Label_Password}" Watermark="{x:Static lang:Resources.Label_Password}"
Grid.Row="1" /> Grid.Row="1" />
<ui:InfoBar
Severity="Error"
IsOpen="{Binding LoginError, Converter={x:Static ObjectConverters.IsNotNull}}"
Title="{Binding LoginError.Message}"
Message="{Binding LoginError.Details}"
Grid.Row="2"/>
<controls:HyperlinkIconButton <controls:HyperlinkIconButton
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Content="Forgot Password?" Content="Forgot Password?"
NavigateUri="{x:Static sm:Assets.LykosForgotPasswordUrl}" NavigateUri="{x:Static sm:Assets.LykosForgotPasswordUrl}"
Grid.Row="2"/> Grid.Row="3"/>
</sg:SpacedGrid> </sg:SpacedGrid>
</TabItem> </TabItem>
@ -58,7 +65,7 @@
<sg:SpacedGrid <sg:SpacedGrid
Margin="-4,32,-4,8" Margin="-4,32,-4,8"
RowDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto"
RowSpacing="8"> RowSpacing="8">
<!-- Email --> <!-- Email -->
<TextBox <TextBox
@ -86,6 +93,13 @@
Watermark="{x:Static lang:Resources.Label_ConfirmPassword}" Watermark="{x:Static lang:Resources.Label_ConfirmPassword}"
UseFloatingWatermark="True" UseFloatingWatermark="True"
Grid.Row="3" /> Grid.Row="3" />
<ui:InfoBar
Severity="Error"
IsOpen="{Binding SignupError, Converter={x:Static ObjectConverters.IsNotNull}}"
Title="{Binding SignupError.Message}"
Message="{Binding SignupError.Details}"
Grid.Row="4"/>
</sg:SpacedGrid> </sg:SpacedGrid>
</TabItem> </TabItem>

11
StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml

@ -68,22 +68,24 @@
<sg:SpacedGrid Margin="0,4,0,16" ColumnDefinitions="*,*"> <sg:SpacedGrid Margin="0,4,0,16" ColumnDefinitions="*,*">
<StackPanel Orientation="Horizontal" Spacing="16"> <StackPanel Orientation="Horizontal" Spacing="16">
<fluentIcons:SymbolIcon <fluentIcons:SymbolIcon
IsVisible="{Binding LykosProfileImageUrl, Converter={x:Static StringConverters.IsNullOrEmpty}}"
Width="64" Width="64"
Height="64" Height="64"
IsFilled="True" IsFilled="True"
FontSize="64" FontSize="64"
Symbol="Person" /> Symbol="Person" />
<!--<controls:BetterAdvancedImage <controls:BetterAdvancedImage
HorizontalAlignment="Left" HorizontalAlignment="Left"
Source="Avares://StabilityMatrix.Avalonia/Assets/noimage.png" IsVisible="{Binding LykosProfileImageUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding LykosProfileImageUrl}"
Width="64" Width="64"
Height="64" Height="64"
CornerRadius="8" />--> CornerRadius="8" />
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Margin="-1,0,0,0" Margin="-1,0,0,0"
Text="{Binding LykosUser.AccountId, FallbackValue=''}" Text="{Binding LykosUser.Account.Name, FallbackValue=''}"
Theme="{DynamicResource SubtitleTextBlockStyle}" /> Theme="{DynamicResource SubtitleTextBlockStyle}" />
<TextBlock Text="{Binding LykosUser.Id, FallbackValue=''}" /> <TextBlock Text="{Binding LykosUser.Id, FallbackValue=''}" />
</StackPanel> </StackPanel>
@ -99,6 +101,7 @@
<controls:SettingsAccountLinkExpander.IconSource> <controls:SettingsAccountLinkExpander.IconSource>
<ui:ImageIconSource Source="{StaticResource BrandsLykos}" /> <ui:ImageIconSource Source="{StaticResource BrandsLykos}" />
</controls:SettingsAccountLinkExpander.IconSource> </controls:SettingsAccountLinkExpander.IconSource>
</controls:SettingsAccountLinkExpander> </controls:SettingsAccountLinkExpander>
<sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4"> <sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">

12
StabilityMatrix.Core/Api/ILykosAuthApi.cs

@ -10,8 +10,18 @@ public interface ILykosAuthApi
[Get("/api/Users/{email}")] [Get("/api/Users/{email}")]
Task<GetUserResponse> GetUser(string email, CancellationToken cancellationToken = default); Task<GetUserResponse> GetUser(string email, CancellationToken cancellationToken = default);
[Headers("Authorization: Bearer")]
[Get("/api/Users/me")]
Task<GetUserResponse> GetUserSelf(CancellationToken cancellationToken = default);
[Post("/api/Accounts")]
Task<LykosAccountTokens> PostAccount(
[Body] PostAccountRequest request,
CancellationToken cancellationToken = default
);
[Post("/api/Login")] [Post("/api/Login")]
Task<PostLoginResponse> PostLogin( Task<LykosAccountTokens> PostLogin(
[Body] PostLoginRequest request, [Body] PostLoginRequest request,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );

12
StabilityMatrix.Core/Exceptions/AppException.cs

@ -5,5 +5,17 @@ namespace StabilityMatrix.Core.Exceptions;
/// </summary> /// </summary>
public class AppException : ApplicationException public class AppException : ApplicationException
{ {
public override string Message { get; }
public string? Details { get; init; } public string? Details { get; init; }
public AppException(string message)
{
Message = message;
}
public AppException(string message, string details)
{
Message = message;
Details = details;
}
} }

2
StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs

@ -2,7 +2,7 @@
public record GetUserResponse( public record GetUserResponse(
string Id, string Id,
string AccountId, LykosAccount Account,
int UserLevel, int UserLevel,
string PatreonId, string PatreonId,
bool IsEmailVerified bool IsEmailVerified

3
StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs

@ -0,0 +1,3 @@
namespace StabilityMatrix.Core.Models.Api.Lykos;
public record LykosAccount(string Id, string Name);

3
StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs

@ -0,0 +1,3 @@
namespace StabilityMatrix.Core.Models.Api.Lykos;
public record LykosAccountTokens(string AccessToken, string RefreshToken);

8
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
);

3
StabilityMatrix.Core/Models/Api/Lykos/PostLoginResponse.cs

@ -1,3 +0,0 @@
namespace StabilityMatrix.Core.Models.Api.Lykos;
public record PostLoginResponse(string AccessToken, string RefreshToken);
Loading…
Cancel
Save