Settings - Added account management

This commit is contained in:
RobertBeekman committed 2024-02-08 20:58:16 +01:00
1 parent 3952a268d1
commit a2fe5d5d08
42 files changed
+681 -30

No files matched your search

@@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="320" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.ChangeEmailAddressView"
x:DataType="account:ChangeEmailAddressViewModel">
<StackPanel Spacing="5" Width="300">
<Label>New email address</Label>
<TextBox Text="{CompiledBinding EmailAddress}" />
<TextBlock Classes="subtitle" Margin="0 5 0 0" TextWrapping="Wrap">An email will be sent to the new address to confirm the change.</TextBlock>
</StackPanel>
</UserControl>
@@ -0,0 +1,16 @@
using Artemis.UI.Shared.Extensions;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings.Account;
public partial class ChangeEmailAddressView : ReactiveUserControl<ChangeEmailAddressViewModel>
{
public ChangeEmailAddressView()
{
InitializeComponent();
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
}
}
@@ -0,0 +1,50 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Settings.Account;
public partial class ChangeEmailAddressViewModel : ContentDialogViewModelBase
{
private readonly IUserManagementService _userManagementService;
private readonly IWindowService _windowService;
[Notify] private string _emailAddress = string.Empty;
public ChangeEmailAddressViewModel(IUserManagementService userManagementService, IAuthenticationService authenticationService, IWindowService windowService)
{
_userManagementService = userManagementService;
_windowService = windowService;
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
string? currentEmail = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Email)?.Value;
this.ValidationRule(vm => vm.EmailAddress, e => !string.IsNullOrWhiteSpace(e),
"You must specify a new email address");
this.ValidationRule(vm => vm.EmailAddress, e => string.IsNullOrWhiteSpace(e) || new EmailAddressAttribute().IsValid(e),
"You must specify a valid email address");
this.ValidationRule(vm => vm.EmailAddress,
e => string.IsNullOrWhiteSpace(e) || currentEmail == null || !string.Equals(e, currentEmail, StringComparison.InvariantCultureIgnoreCase),
"New email address must be different from the old one");
}
public ReactiveCommand<Unit, Unit> Submit { get; }
private async Task ExecuteSubmit(CancellationToken cts)
{
ApiResult result = await _userManagementService.ChangeEmailAddress(EmailAddress, cts);
if (result.IsSuccess)
await _windowService.ShowConfirmContentDialog("Confirmation required", "Before being applied, you must confirm your new email address. Please check your inbox.", cancel: null);
else
await _windowService.ShowConfirmContentDialog("Failed to update email address", result.Message ?? "An unexpected error occured", cancel: null);
}
}
@@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.ChangePasswordView"
x:DataType="account:ChangePasswordViewModel">
<StackPanel Spacing="5" Width="300">
<Label>Current password</Label>
<TextBox Text="{CompiledBinding CurrentPassword}" PasswordChar="&#x25CF;"/>
<Label>New password</Label>
<TextBox Text="{CompiledBinding NewPassword}" PasswordChar="&#x25CF;"/>
<Label>New password confirmation</Label>
<TextBox Text="{CompiledBinding NewPasswordConfirmation}" PasswordChar="&#x25CF;"/>
</StackPanel>
</UserControl>
@@ -0,0 +1,17 @@
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings.Account;
public partial class ChangePasswordView : ReactiveUserControl<ChangePasswordViewModel>
{
public ChangePasswordView()
{
InitializeComponent();
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
}
}
@@ -0,0 +1,47 @@
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Settings.Account;
public partial class ChangePasswordViewModel : ContentDialogViewModelBase
{
private readonly IUserManagementService _userManagementService;
private readonly IWindowService _windowService;
[Notify] private string _currentPassword = string.Empty;
[Notify] private string _newPassword = string.Empty;
[Notify] private string _newPasswordConfirmation = string.Empty;
public ChangePasswordViewModel(IUserManagementService userManagementService, IWindowService windowService)
{
_userManagementService = userManagementService;
_windowService = windowService;
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
this.ValidationRule(vm => vm.CurrentPassword, e => !string.IsNullOrWhiteSpace(e), "You must specify your current password");
this.ValidationRule(vm => vm.NewPassword, e => !string.IsNullOrWhiteSpace(e), "You must specify a new password");
this.ValidationRule(
vm => vm.NewPasswordConfirmation,
this.WhenAnyValue(vm => vm.NewPassword, vm => vm.NewPasswordConfirmation, (password, confirmation) => password == confirmation),
"The passwords must match"
);
}
public ReactiveCommand<Unit, Unit> Submit { get; }
private async Task ExecuteSubmit(CancellationToken cts)
{
ApiResult result = await _userManagementService.ChangePassword(CurrentPassword, NewPassword, cts);
if (result.IsSuccess)
await _windowService.ShowConfirmContentDialog("Password changed", "Your password has been changed", cancel: null);
else
await _windowService.ShowConfirmContentDialog("Failed to change password", result.Message ?? "An unexpected error occured", cancel: null);
}
}
@@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="320" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.RemoveAccountView"
x:DataType="account:RemoveAccountViewModel">
<StackPanel Spacing="5" Width="350">
<Label>Please enter your email address to confirm</Label>
<TextBox Text="{CompiledBinding EmailAddress}"/>
<TextBlock Classes="danger" Margin="0 5 0 0" TextWrapping="Wrap">This is a destructive action that cannot be undone.</TextBlock>
</StackPanel>
</UserControl>
@@ -0,0 +1,16 @@
using Artemis.UI.Shared.Extensions;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings.Account;
public partial class RemoveAccountView : ReactiveUserControl<RemoveAccountViewModel>
{
public RemoveAccountView()
{
InitializeComponent();
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
}
}
@@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Settings.Account;
public partial class RemoveAccountViewModel : ContentDialogViewModelBase
{
private readonly IUserManagementService _userManagementService;
private readonly IAuthenticationService _authenticationService;
private readonly IWindowService _windowService;
[Notify] private string _emailAddress = string.Empty;
public RemoveAccountViewModel(IUserManagementService userManagementService, IAuthenticationService authenticationService, IWindowService windowService)
{
_userManagementService = userManagementService;
_authenticationService = authenticationService;
_windowService = windowService;
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
string? currentEmail = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Email)?.Value;
this.ValidationRule(vm => vm.EmailAddress, e => !string.IsNullOrWhiteSpace(e), "You must enter your email address");
this.ValidationRule(vm => vm.EmailAddress,
e => string.IsNullOrWhiteSpace(e) || string.Equals(e, currentEmail, StringComparison.InvariantCultureIgnoreCase),
"The entered email address is not correct");
}
public ReactiveCommand<Unit, Unit> Submit { get; }
private async Task ExecuteSubmit(CancellationToken cts)
{
ApiResult result = await _userManagementService.RemoveAccount(cts);
if (result.IsSuccess)
{
await _windowService.ShowConfirmContentDialog("Account removed", "Hopefully we'll see you again!", cancel: null);
_authenticationService.Logout();
}
else
await _windowService.ShowConfirmContentDialog("Failed to remove account", result.Message ?? "An unexpected error occured", cancel: null);
}
}
@@ -26,6 +26,7 @@ public partial class SettingsViewModel : RoutableHostScreen<RoutableScreen>, IMa
new("Plugins", "settings/plugins"),
new("Devices", "settings/devices"),
new("Releases", "settings/releases"),
new("Account", "settings/account"),
new("About", "settings/about"),
};
@@ -0,0 +1,121 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:loaders="clr-namespace:AsyncImageLoader.Loaders;assembly=AsyncImageLoader.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
x:DataType="settings:AccountTabViewModel">
<Panel>
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock>
<TextBlock>
<Run>In order to manage your account you must be logged in.</Run>
<LineBreak />
<Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie>
<TextBlock>
<Run>Click Log In below to (create an account) and log in.</Run>
<LineBreak />
<Run>You'll also be able to log in with Google or Discord.</Run>
</TextBlock>
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button>
</StackPanel>
<ScrollViewer IsVisible="{CompiledBinding IsLoggedIn^}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel>
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*" VerticalAlignment="Top">
<Border Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" VerticalAlignment="Top" CornerRadius="92" Width="92" Height="92" Margin="0 0 15 0" ClipToBounds="True">
<asyncImageLoader:AdvancedImage Source="{CompiledBinding AvatarUrl}">
<asyncImageLoader:AdvancedImage.Loader>
<loaders:BaseWebImageLoader />
</asyncImageLoader:AdvancedImage.Loader>
</asyncImageLoader:AdvancedImage>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" />
<TextBlock Classes="subtitle" Grid.Column="1" Grid.Row="1" Padding="0" VerticalAlignment="Top" Text="{CompiledBinding Email}" />
</Grid>
<TextBlock Classes="card-title">
Account management
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
Change avatar
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button Width="150" Content="Choose image" Command="{CompiledBinding ChangeAvatar}" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
Email address
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button Width="150" Content=" Change email" Command="{CompiledBinding ChangeEmailAddress}" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
Password
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
Remove account
</TextBlock>
<TextBlock Classes="subtitle">
Permanently remove your account, this cannot be undone
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button Classes="danger" Width="150" Content="Remove account" Command="{CompiledBinding RemoveAccount}" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- <TextBlock Classes="card-title"> -->
<!-- Personal access tokens -->
<!-- </TextBlock> -->
<!-- <Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0"> -->
<!-- <StackPanel> -->
<!-- <TextBlock>TODO :)</TextBlock> -->
<!-- </StackPanel> -->
<!-- </Border> -->
</StackPanel>
</ScrollViewer>
</Panel>
</UserControl>
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings;
public partial class AccountTabView : ReactiveUserControl<AccountTabViewModel>
{
public AccountTabView()
{
InitializeComponent();
}
}
@@ -0,0 +1,105 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Settings.Account;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings;
public partial class AccountTabViewModel : RoutableScreen
{
private readonly IWindowService _windowService;
private readonly IAuthenticationService _authenticationService;
private readonly IUserManagementService _userManagementService;
[Notify(Setter.Private)] private string? _name;
[Notify(Setter.Private)] private string? _email;
[Notify(Setter.Private)] private string? _avatarUrl;
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
{
_windowService = windowService;
_authenticationService = authenticationService;
_userManagementService = userManagementService;
_authenticationService.AutoLogin(true);
DisplayName = "Account";
IsLoggedIn = _authenticationService.IsLoggedIn;
this.WhenActivated(d => _authenticationService.IsLoggedIn.Subscribe(_ => LoadCurrentUser()).DisposeWith(d));
}
public IObservable<bool> IsLoggedIn { get; }
public async Task Login()
{
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync();
}
public async Task ChangeAvatar()
{
string[]? result = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithBitmaps()).ShowAsync();
if (result == null)
return;
try
{
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{Guid.Empty}";
await using FileStream fileStream = new(result.First(), FileMode.Open);
ApiResult changeResult = await _userManagementService.ChangeAvatar(fileStream, CancellationToken.None);
if (!changeResult.IsSuccess)
await _windowService.ShowConfirmContentDialog("Failed to change image", changeResult.Message ?? "An unexpected error occured", cancel: null);
}
finally
{
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
}
}
public async Task ChangeEmailAddress()
{
await _windowService.CreateContentDialog().WithTitle("Change email address")
.WithViewModel(out ChangeEmailAddressViewModel vm)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
public async Task ChangePasswordAddress()
{
await _windowService.CreateContentDialog().WithTitle("Change password")
.WithViewModel(out ChangePasswordViewModel vm)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
public async Task RemoveAccount()
{
await _windowService.CreateContentDialog().WithTitle("Remove account")
.WithViewModel(out RemoveAccountViewModel vm)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Permanently remove account").WithCommand(vm.Submit))
.ShowAsync();
}
private void LoadCurrentUser()
{
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
}
}