- fix: auth logic part 1

This commit is contained in:
2025-08-06 21:29:00 +03:00
parent f6a15e9c45
commit 6a6bb4f27c
26 changed files with 513 additions and 259 deletions

View File

@@ -0,0 +1,20 @@
using System;
using System.Security.Cryptography;
using System.Text;
using Avalonia.Media;
namespace Nebula.Launcher.ViewModels.Pages;
public static class ColorUtils
{
public static Color GetColorFromString(string input)
{
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
var r = byte.Clamp(hash[0], 10, 200);
var g = byte.Clamp(hash[1], 10, 100);
var b = byte.Clamp(hash[2], 10, 100);
return Color.FromArgb(Byte.MaxValue, r, g, b);
}
}

View File

@@ -2,6 +2,8 @@ using System;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Nebula.Launcher.ViewModels.Pages;
using Color = System.Drawing.Color;
namespace Nebula.Launcher.Converters; namespace Nebula.Launcher.Converters;
@@ -20,4 +22,7 @@ public static class TypeConverters
if (iconKey == null) return null; if (iconKey == null) return null;
return new Avalonia.Media.Imaging.Bitmap(AssetLoader.Open(new Uri($"avares://Nebula.Launcher/Assets/error_presentation/{iconKey}.png"))); return new Avalonia.Media.Imaging.Bitmap(AssetLoader.Open(new Uri($"avares://Nebula.Launcher/Assets/error_presentation/{iconKey}.png")));
}); });
public static FuncValueConverter<string, Avalonia.Media.Color> NameColorRepresentation { get; } =
new(ColorUtils.GetColorFromString);
} }

View File

@@ -3,6 +3,8 @@ using System.Globalization;
using Nebula.Launcher.Models; using Nebula.Launcher.Models;
using Nebula.Launcher.Models.Auth; using Nebula.Launcher.Models.Auth;
using Nebula.Shared.ConfigMigrations; using Nebula.Shared.ConfigMigrations;
using Nebula.Shared.Configurations;
using Nebula.Shared.Configurations.Migrations;
using Nebula.Shared.Services; using Nebula.Shared.Services;
namespace Nebula.Launcher; namespace Nebula.Launcher;

View File

@@ -8,6 +8,5 @@ namespace Nebula.Launcher.Models.Auth;
public sealed record ProfileAuthCredentials( public sealed record ProfileAuthCredentials(
AuthTokenCredentials Credentials, AuthTokenCredentials Credentials,
string AuthName, string AuthName,
Color Color,
[property: JsonIgnore] ICommand OnSelect = default!, [property: JsonIgnore] ICommand OnSelect = default!,
[property: JsonIgnore] ICommand OnDelete = default!); [property: JsonIgnore] ICommand OnDelete = default!);

View File

@@ -35,7 +35,7 @@ public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverS
{ {
var baseStart = await base.GetProcessStartInfo(); var baseStart = await base.GetProcessStartInfo();
var authProv = accountInfoViewModel.Credentials; var authProv = accountInfoViewModel.Credentials.Value;
if(authProv is null) if(authProv is null)
throw new Exception("Client is without selected auth"); throw new Exception("Client is without selected auth");

View File

@@ -41,12 +41,12 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private bool _popup; [ObservableProperty] private bool _popup;
[ObservableProperty] private ListItemTemplate? _selectedListItem; [ObservableProperty] private ListItemTemplate? _selectedListItem;
public bool IsLoggedIn => AccountInfoViewModel.Credentials is not null; public bool IsLoggedIn => AccountInfoViewModel.Credentials.Value is not null;
public string LoginText => LocalisationService.GetString("auth-current-login-name", public string LoginText => LocalisationService.GetString("auth-current-login-name",
new Dictionary<string, object> new Dictionary<string, object>
{ {
{ "login", AccountInfoViewModel.Credentials?.Login ?? "" }, { "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" },
{ "auth_server", AccountInfoViewModel.CurrentAuthServerName} { "auth_server", AccountInfoViewModel.CurrentAuthServerName}
}); });

View File

@@ -3,16 +3,13 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Nebula.Launcher.Models.Auth; using Nebula.Launcher.Models.Auth;
using Nebula.Launcher.Services; using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views.Pages; using Nebula.Launcher.Views.Pages;
using Nebula.Shared.Configurations;
using Nebula.Shared.Models.Auth; using Nebula.Shared.Models.Auth;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging; using Nebula.Shared.Services.Logging;
@@ -33,7 +30,6 @@ public partial class AccountInfoViewModel : ViewModelBase
[ObservableProperty] private string _currentPassword = string.Empty; [ObservableProperty] private string _currentPassword = string.Empty;
[ObservableProperty] private bool _isLogged; [ObservableProperty] private bool _isLogged;
[ObservableProperty] private bool _doRetryAuth; [ObservableProperty] private bool _doRetryAuth;
[ObservableProperty] private AuthTokenCredentials? _credentials;
[ObservableProperty] private AuthServerCredentials _authItemSelect; [ObservableProperty] private AuthServerCredentials _authItemSelect;
private bool _isProfilesEmpty; private bool _isProfilesEmpty;
@@ -45,15 +41,12 @@ public partial class AccountInfoViewModel : ViewModelBase
public ObservableCollection<ProfileAuthCredentials> Accounts { get; } = new(); public ObservableCollection<ProfileAuthCredentials> Accounts { get; } = new();
public ObservableCollection<AuthServerCredentials> AuthUrls { get; } = new(); public ObservableCollection<AuthServerCredentials> AuthUrls { get; } = new();
public string CurrentAuthServerName => GetServerAuthName(Credentials); public string CurrentAuthServerName => GetServerAuthName(Credentials.Value);
public ComplexConVarBinder<AuthTokenCredentials?> Credentials;
private ILogger _logger; private ILogger _logger;
partial void OnCredentialsChanged(AuthTokenCredentials? value)
{
OnPropertyChanged(nameof(CurrentAuthServerName));
}
//Design think //Design think
protected override void InitialiseInDesignMode() protected override void InitialiseInDesignMode()
{ {
@@ -62,41 +55,15 @@ public partial class AccountInfoViewModel : ViewModelBase
AddAccount(new AuthTokenCredentials(Guid.Empty, LoginToken.Empty, "Binka", "example.com")); AddAccount(new AuthTokenCredentials(Guid.Empty, LoginToken.Empty, "Binka", "example.com"));
AddAccount(new AuthTokenCredentials(Guid.Empty, LoginToken.Empty, "Binka", "")); AddAccount(new AuthTokenCredentials(Guid.Empty, LoginToken.Empty, "Binka", ""));
} }
//Real think //Real think
protected override void Initialise() protected override void Initialise()
{ {
_logger = DebugService.GetLogger(this); _logger = DebugService.GetLogger(this);
Credentials = new AuthTokenCredentialsVar(this);
Task.Run(ReadAuthConfig); Task.Run(ReadAuthConfig);
} }
public async void AuthByProfile(ProfileAuthCredentials credentials)
{
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
message.InfoText = LocalisationService.GetString("auth-try-auth-profile");
message.IsInfoClosable = false;
PopupMessageService.Popup(message);
try
{
await CatchAuthError(async () => await TryAuth(credentials.Credentials), () => message.Dispose());
}
catch (Exception ex)
{
CurrentLogin = credentials.Credentials.Login;
CurrentAuthServer = credentials.Credentials.AuthServer;
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex);
_logger.Error(unexpectedError);
PopupMessageService.Popup(unexpectedError);
return;
}
ConfigurationService.SetConfigValue(LauncherConVar.AuthCurrent, Credentials);
message.Dispose();
}
public void DoAuth(string? code = null) public void DoAuth(string? code = null)
{ {
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>(); var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
@@ -114,54 +81,33 @@ public partial class AccountInfoViewModel : ViewModelBase
Task.Run(async () => Task.Run(async () =>
{ {
Exception? exception = null; Exception? exception = null;
AuthTokenCredentials? credentials = null;
foreach (var server in serverCandidates) foreach (var server in serverCandidates)
{ {
try try
{ {
await CatchAuthError(async () => await TryAuth(CurrentLogin, CurrentPassword, server, code), ()=> message.Dispose()); credentials = await AuthService.Auth(CurrentLogin, CurrentPassword, server, code);
break; break;
} }
catch (Exception ex) catch (Exception ex)
{ {
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex); exception = new Exception(LocalisationService.GetString("auth-error"), ex);
_logger.Error(unexpectedError);
PopupMessageService.Popup(unexpectedError);
} }
} }
message.Dispose(); message.Dispose();
if (!IsLogged) if (credentials is null)
{ {
PopupMessageService.Popup(exception ?? new Exception(LocalisationService.GetString("auth-error"))); PopupMessageService.Popup(exception ?? new Exception(LocalisationService.GetString("auth-error")));
return;
} }
Credentials.Value = credentials;
}); });
} }
private async Task TryAuth(AuthTokenCredentials authTokenCredentials)
{
CurrentLogin = authTokenCredentials.Login;
CurrentAuthServer = authTokenCredentials.AuthServer;
await SetAuth(authTokenCredentials);
IsLogged = true;
}
private async Task SetAuth(AuthTokenCredentials authTokenCredentials)
{
await AuthService.EnsureToken(authTokenCredentials);
Credentials = authTokenCredentials;
}
private async Task TryAuth(string login, string password, string authServer, string? code)
{
Credentials = await AuthService.Auth(login, password, authServer, code);
CurrentLogin = login;
CurrentPassword = password;
CurrentAuthServer = authServer;
IsLogged = true;
ConfigurationService.SetConfigValue(LauncherConVar.AuthCurrent, Credentials);
}
private async Task CatchAuthError(Func<Task> a, Action? onError) private async Task CatchAuthError(Func<Task> a, Action? onError)
{ {
DoRetryAuth = false; DoRetryAuth = false;
@@ -185,8 +131,18 @@ public partial class AccountInfoViewModel : ViewModelBase
case AuthenticateDenyCode.InvalidCredentials: case AuthenticateDenyCode.InvalidCredentials:
PopupError(LocalisationService.GetString("auth-invalid-credentials"), e); PopupError(LocalisationService.GetString("auth-invalid-credentials"), e);
break; break;
case AuthenticateDenyCode.AccountLocked:
PopupError(LocalisationService.GetString("auth-account-locked"), e);
break;
case AuthenticateDenyCode.AccountUnconfirmed:
PopupError(LocalisationService.GetString("auth-account-unconfirmed"), e);
break;
case AuthenticateDenyCode.None:
PopupError(LocalisationService.GetString("auth-none"),e);
break;
default: default:
throw; PopupError(LocalisationService.GetString("auth-error-fuck"), e);
break;
} }
} }
catch (HttpRequestException e) catch (HttpRequestException e)
@@ -198,17 +154,41 @@ public partial class AccountInfoViewModel : ViewModelBase
PopupError(LocalisationService.GetString("auth-connection-error"), e); PopupError(LocalisationService.GetString("auth-connection-error"), e);
DoRetryAuth = true; DoRetryAuth = true;
break; break;
case HttpRequestError.NameResolutionError: case HttpRequestError.NameResolutionError:
PopupError(LocalisationService.GetString("auth-name-resolution-error"), e); PopupError(LocalisationService.GetString("auth-name-resolution-error"), e);
DoRetryAuth = true; DoRetryAuth = true;
break; break;
case HttpRequestError.SecureConnectionError: case HttpRequestError.SecureConnectionError:
PopupError(LocalisationService.GetString("auth-secure-error"), e); PopupError(LocalisationService.GetString("auth-secure-error"), e);
DoRetryAuth = true; DoRetryAuth = true;
break; break;
case HttpRequestError.UserAuthenticationError:
PopupError(LocalisationService.GetString("auth-user-authentication-error"), e);
break;
case HttpRequestError.Unknown:
PopupError(LocalisationService.GetString("auth-unknown"), e);
break;
case HttpRequestError.HttpProtocolError:
PopupError(LocalisationService.GetString("auth-http-protocol-error"), e);
break;
case HttpRequestError.ExtendedConnectNotSupported:
PopupError(LocalisationService.GetString("auth-extended-connect-not-support"), e);
break;
case HttpRequestError.VersionNegotiationError:
PopupError(LocalisationService.GetString("auth-version-negotiation-error"), e);
break;
case HttpRequestError.ProxyTunnelError:
PopupError(LocalisationService.GetString("auth-proxy-tunnel-error"), e);
break;
case HttpRequestError.InvalidResponse:
PopupError(LocalisationService.GetString("auth-invalid-response"), e);
break;
case HttpRequestError.ResponseEnded:
PopupError(LocalisationService.GetString("auth-response-ended"), e);
break;
case HttpRequestError.ConfigurationLimitExceeded:
PopupError(LocalisationService.GetString("auth-configuration-limit-exceeded"), e);
break;
default: default:
var authError = new Exception(LocalisationService.GetString("auth-error"), e); var authError = new Exception(LocalisationService.GetString("auth-error"), e);
_logger.Error(authError); _logger.Error(authError);
@@ -225,8 +205,7 @@ public partial class AccountInfoViewModel : ViewModelBase
public void Logout() public void Logout()
{ {
IsLogged = false; Credentials.Value = null;
Credentials = null;
CurrentAuthServer = ""; CurrentAuthServer = "";
} }
@@ -247,14 +226,13 @@ public partial class AccountInfoViewModel : ViewModelBase
private void AddAccount(AuthTokenCredentials credentials) private void AddAccount(AuthTokenCredentials credentials)
{ {
var onDelete = new DelegateCommand<ProfileAuthCredentials>(OnDeleteProfile); var onDelete = new DelegateCommand<ProfileAuthCredentials>(OnDeleteProfile);
var onSelect = new DelegateCommand<ProfileAuthCredentials>(AuthByProfile); var onSelect = new DelegateCommand<ProfileAuthCredentials>((p) => Credentials.Value = p.Credentials);
var serverName = GetServerAuthName(credentials); var serverName = GetServerAuthName(credentials);
var alpm = new ProfileAuthCredentials( var alpm = new ProfileAuthCredentials(
credentials, credentials,
serverName, serverName,
ColorUtils.GetColorFromString(credentials.AuthServer),
onSelect, onSelect,
onDelete); onDelete);
@@ -264,62 +242,77 @@ public partial class AccountInfoViewModel : ViewModelBase
Accounts.Add(alpm); Accounts.Add(alpm);
} }
private void ReadAuthConfig() private async void ReadAuthConfig()
{ {
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>(); var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
message.InfoText = LocalisationService.GetString("auth-config-read"); message.InfoText = LocalisationService.GetString("auth-config-read");
message.IsInfoClosable = false; message.IsInfoClosable = false;
PopupMessageService.Popup(message); PopupMessageService.Popup(message);
_logger.Log("Reading auth config");
AuthUrls.Clear(); AuthUrls.Clear();
var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!; var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!;
foreach (var url in authUrls) AuthUrls.Add(url); foreach (var url in authUrls) AuthUrls.Add(url);
if(authUrls.Length > 0) AuthItemSelect = authUrls[0]; if(authUrls.Length > 0) AuthItemSelect = authUrls[0];
var profileCandidates = new List<AuthTokenCredentials>();
foreach (var profile in foreach (var profile in
ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!) ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!)
AddAccount(profile); {
_logger.Log($"Reading profile {profile.Login}");
var checkedCredit = await CheckOrRenewToken(profile);
if(checkedCredit is null)
{
_logger.Error($"Profile {profile.Login} is not available");
continue;
}
_logger.Log($"Profile {profile.Login} is available");
profileCandidates.Add(checkedCredit);
AddAccount(checkedCredit);
}
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, profileCandidates.ToArray());
if (Accounts.Count == 0) UpdateAuthMenu(); if (Accounts.Count == 0) UpdateAuthMenu();
message.Dispose(); message.Dispose();
DoCurrentAuth();
} }
public async void DoCurrentAuth() public void DoCurrentAuth()
{ {
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>(); DoAuth();
message.InfoText = LocalisationService.GetString("auth-try-auth-config");
message.IsInfoClosable = false;
PopupMessageService.Popup(message);
var currProfile = ConfigurationService.GetConfigValue(LauncherConVar.AuthCurrent);
if (currProfile != null)
{
try
{
await CatchAuthError(async () => await TryAuth(currProfile), () => message.Dispose());
}
catch (Exception ex)
{
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex);
_logger.Error(unexpectedError);
PopupMessageService.Popup(unexpectedError);
return;
}
}
message.Dispose();
} }
[RelayCommand] private async Task<AuthTokenCredentials?> CheckOrRenewToken(AuthTokenCredentials? authTokenCredentials)
private void OnSaveProfile()
{ {
if(Credentials is null) return; if(authTokenCredentials is null)
return null;
AddAccount(Credentials); if (authTokenCredentials.Token.ExpireTime < DateTime.Now.AddDays(2))
return authTokenCredentials;
try
{
_logger.Log($"Renewing token for {authTokenCredentials.Login}");
return await AuthService.Refresh(authTokenCredentials);
}
catch (Exception e)
{
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), e);
_logger.Error(unexpectedError);
PopupMessageService.Popup(unexpectedError);
return null;
}
}
public void OnSaveProfile()
{
if(Credentials.Value is null) return;
AddAccount(Credentials.Value);
_isProfilesEmpty = Accounts.Count == 0; _isProfilesEmpty = Accounts.Count == 0;
UpdateAuthMenu(); UpdateAuthMenu();
DirtyProfile(); DirtyProfile();
@@ -343,15 +336,13 @@ public partial class AccountInfoViewModel : ViewModelBase
messageView.IsInfoClosable = true; messageView.IsInfoClosable = true;
PopupMessageService.Popup(messageView); PopupMessageService.Popup(messageView);
} }
[RelayCommand] public void OnExpandAuthUrl()
private void OnExpandAuthUrl()
{ {
AuthUrlConfigExpand = !AuthUrlConfigExpand; AuthUrlConfigExpand = !AuthUrlConfigExpand;
} }
[RelayCommand] public void OnExpandAuthView()
private void OnExpandAuthView()
{ {
AuthMenuExpand = !AuthMenuExpand; AuthMenuExpand = !AuthMenuExpand;
UpdateAuthMenu(); UpdateAuthMenu();
@@ -362,18 +353,65 @@ public partial class AccountInfoViewModel : ViewModelBase
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles,
Accounts.Select(a => a.Credentials).ToArray()); Accounts.Select(a => a.Credentials).ToArray());
} }
public sealed class AuthTokenCredentialsVar(AccountInfoViewModel accountInfoViewModel)
: ComplexConVarBinder<AuthTokenCredentials?>(
accountInfoViewModel.ConfigurationService.SubscribeVarChanged(LauncherConVar.AuthCurrent))
{
protected override async Task<AuthTokenCredentials?> OnValueChange(AuthTokenCredentials? currProfile)
{
if (currProfile is null)
{
accountInfoViewModel.IsLogged = false;
accountInfoViewModel._logger.Log("clearing credentials");
accountInfoViewModel.OnPropertyChanged(nameof(CurrentAuthServerName));
return null;
}
var message = accountInfoViewModel.ViewHelperService.GetViewModel<InfoPopupViewModel>();
message.InfoText = LocalisationService.GetString("auth-try-auth-config");
message.IsInfoClosable = false;
accountInfoViewModel.PopupMessageService.Popup(message);
accountInfoViewModel._logger.Log($"trying auth with {currProfile.Login}");
var errorRun = false;
//currProfile = await CheckOrRenewToken(currProfile);
try
{
await accountInfoViewModel.CatchAuthError(async () =>
{
await accountInfoViewModel.AuthService.EnsureToken(currProfile);
}, () =>
{
message.Dispose();
errorRun = true;
});
message.Dispose();
}
catch (Exception ex)
{
accountInfoViewModel.CurrentLogin = currProfile.Login;
accountInfoViewModel.CurrentAuthServer = currProfile.AuthServer;
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex);
accountInfoViewModel._logger.Error(unexpectedError);
accountInfoViewModel.PopupMessageService.Popup(unexpectedError);
errorRun = true;
}
if (errorRun)
{
accountInfoViewModel.IsLogged = false;
return null;
}
accountInfoViewModel.IsLogged = true;
accountInfoViewModel.OnPropertyChanged(nameof(CurrentAuthServerName));
return currProfile;
}
}
} }
public static class ColorUtils
{
public static Color GetColorFromString(string input)
{
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
var r = byte.Clamp(hash[0], 10, 200);
var g = byte.Clamp(hash[1], 10, 100);
var b = byte.Clamp(hash[2], 10, 100);
return Color.FromArgb(Byte.MaxValue, r, g, b);
}
}

View File

@@ -15,6 +15,7 @@ using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views.Pages; using Nebula.Launcher.Views.Pages;
using Nebula.Shared; using Nebula.Shared;
using Nebula.Shared.Configurations;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.ViewHelper; using Nebula.Shared.ViewHelper;

View File

@@ -174,7 +174,7 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis
private async Task RunInstanceAsync(bool ignoreLoginCredentials = false) private async Task RunInstanceAsync(bool ignoreLoginCredentials = false)
{ {
if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials is null) if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials.Value is null)
{ {
var warningContext = ViewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>() var warningContext = ViewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
.WithServerEntry(this); .WithServerEntry(this);

View File

@@ -10,7 +10,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages" xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:auth="clr-namespace:Nebula.Launcher.Models.Auth"> xmlns:auth="clr-namespace:Nebula.Launcher.Models.Auth"
xmlns:converters="clr-namespace:Nebula.Launcher.Converters">
<Design.DataContext> <Design.DataContext>
<pages:AccountInfoViewModel /> <pages:AccountInfoViewModel />
</Design.DataContext> </Design.DataContext>
@@ -46,7 +47,8 @@
Margin="5,5,5,0"> Margin="5,5,5,0">
<Border.Background> <Border.Background>
<LinearGradientBrush EndPoint="100%,50%" StartPoint="20%,50%"> <LinearGradientBrush EndPoint="100%,50%" StartPoint="20%,50%">
<GradientStop Color="{Binding Color}" Offset="0.0" /> <GradientStop Color="{Binding Credentials.AuthServer,
Converter={x:Static converters:TypeConverters.NameColorRepresentation}}" Offset="0.0" />
<GradientStop Color="#222222" Offset="1.0" /> <GradientStop Color="#222222" Offset="1.0" />
</LinearGradientBrush> </LinearGradientBrush>
</Border.Background> </Border.Background>
@@ -138,7 +140,7 @@
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<customControls:LocalizedLabel VerticalAlignment="Center" LocalId="account-auth-server"/> <customControls:LocalizedLabel VerticalAlignment="Center" LocalId="account-auth-server"/>
<Button Command="{Binding ExpandAuthUrlCommand}" VerticalAlignment="Stretch"> <Button Command="{Binding OnExpandAuthUrl}" VerticalAlignment="Stretch">
<Label>+</Label> <Label>+</Label>
</Button> </Button>
</StackPanel> </StackPanel>
@@ -173,7 +175,7 @@
<customControls:LocalizedLabel LocalId="account-auth-button"/> <customControls:LocalizedLabel LocalId="account-auth-button"/>
</Button> </Button>
</Border> </Border>
<Button Command="{Binding ExpandAuthViewCommand}" HorizontalAlignment="Right"> <Button Command="{Binding OnExpandAuthView}" HorizontalAlignment="Right">
<Label> <Label>
> >
</Label> </Label>
@@ -208,7 +210,7 @@
</Button> </Button>
</Border> </Border>
<Border BoxShadow="{StaticResource DefaultShadow}"> <Border BoxShadow="{StaticResource DefaultShadow}">
<Button Command="{Binding SaveProfileCommand}"> <Button Command="{Binding OnSaveProfile}">
<customControls:LocalizedLabel LocalId="account-auth-save"/> <customControls:LocalizedLabel LocalId="account-auth-save"/>
</Button> </Button>
</Border> </Border>

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nebula.Shared.Configurations.Migrations;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;

View File

@@ -0,0 +1,70 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Nebula.Shared.Configurations;
public abstract class ComplexConVarBinder<T> : INotifyPropertyChanged, INotifyPropertyChanging
{
private readonly ConVarObserver<T> _baseConVar;
private readonly Lock _lock = new();
private readonly SemaphoreSlim _valueChangeSemaphore = new(1, 1);
public T? Value
{
get
{
lock (_lock)
{
return _baseConVar.Value;
}
}
set
{
_ = SetValueAsync(value);
}
}
protected ComplexConVarBinder(ConVarObserver<T> baseConVar)
{
_baseConVar = baseConVar ?? throw new ArgumentNullException(nameof(baseConVar));
_baseConVar.PropertyChanged += BaseConVarOnPropertyChanged;
_baseConVar.PropertyChanging += BaseConVarOnPropertyChanging;
}
private async Task SetValueAsync(T? value)
{
await _valueChangeSemaphore.WaitAsync().ConfigureAwait(false);
try
{
var newValue = await OnValueChange(value).ConfigureAwait(false);
lock (_lock)
{
_baseConVar.Value = newValue;
}
}
finally
{
_valueChangeSemaphore.Release();
}
}
protected abstract Task<T?> OnValueChange(T? newValue);
private void BaseConVarOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
private void BaseConVarOnPropertyChanging(object? sender, PropertyChangingEventArgs e)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Value)));
}
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;
}

View File

@@ -0,0 +1,18 @@
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations;
public class ConVar<T>
{
internal ConfigurationService.OnConfigurationChangedDelegate<T?>? OnValueChanged;
public ConVar(string name, T? defaultValue = default)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
DefaultValue = defaultValue;
}
public string Name { get; }
public Type Type => typeof(T);
public T? DefaultValue { get; }
}

View File

@@ -0,0 +1,25 @@
using Nebula.Shared.Configurations.Migrations;
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations;
public static class ConVarBuilder
{
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
return new ConVar<T>(name, defaultValue);
}
public static ConVar<T> BuildWithMigration<T>(string name, IConfigurationMigration migration, T? defaultValue = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
ConfigurationService.AddConfigurationMigration(migration);
return new ConVar<T>(name, defaultValue);
}
}

View File

@@ -0,0 +1,68 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations;
public sealed class ConVarObserver<T> : IDisposable, INotifyPropertyChanged, INotifyPropertyChanging
{
private readonly ConVar<T> _convar;
private readonly ConfigurationService _configurationService;
private T? _value;
private ConfigurationService.OnConfigurationChangedDelegate<T> _delegate;
public T? Value
{
get => _value;
set => _configurationService.SetConfigValue(_convar, value);
}
public ConVarObserver(ConVar<T> convar, ConfigurationService configurationService)
{
_convar = convar;
_convar.OnValueChanged += OnValueChanged;
_configurationService = configurationService;
_delegate += OnValueChanged;
OnValueChanged(configurationService.GetConfigValue(_convar));
}
private void OnValueChanged(T? value)
{
OnPropertyChanging(nameof(Value));
if(value is null && _value is null)
return;
if (_value is not null && _value.Equals(value))
return;
_value = value;
OnPropertyChanged(nameof(Value));
}
public void Dispose()
{
_convar.OnValueChanged -= OnValueChanged;
}
public bool HasValue()
{
return Value != null;
}
public static implicit operator T? (ConVarObserver<T> convar) => convar.Value;
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,28 @@
using Nebula.Shared.Models;
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations.Migrations;
public abstract class BaseConfigurationMigration<T1,T2> : IConfigurationMigration
{
protected ConVar<T1> OldConVar;
protected ConVar<T2> NewConVar;
public BaseConfigurationMigration(string oldName, string newName)
{
OldConVar = ConVarBuilder.Build<T1>(oldName);
NewConVar = ConVarBuilder.Build<T2>(newName);
}
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler)
{
var oldValue = configurationService.GetConfigValue(OldConVar);
if(oldValue == null) return;
var newValue = await Migrate(serviceProvider, oldValue, loadingHandler);
configurationService.SetConfigValue(NewConVar, newValue);
configurationService.ClearConfigValue(OldConVar);
}
protected abstract Task<T2> Migrate(IServiceProvider serviceProvider, T1 oldValue, ILoadingHandler loadingHandler);
}

View File

@@ -0,0 +1,9 @@
using Nebula.Shared.Models;
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations.Migrations;
public interface IConfigurationMigration
{
public Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler);
}

View File

@@ -0,0 +1,15 @@
using Nebula.Shared.Models;
using Nebula.Shared.Services;
namespace Nebula.Shared.Configurations.Migrations;
public class MigrationQueue(List<IConfigurationMigration> migrations) : IConfigurationMigration
{
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider , ILoadingHandler loadingHandler)
{
foreach (var migration in migrations)
{
await migration.DoMigrate(configurationService, serviceProvider, loadingHandler);
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Nebula.Shared.Configurations.Migrations;
public class MigrationQueueBuilder
{
public static MigrationQueueBuilder Instance => new();
private readonly List<IConfigurationMigration> _migrations = [];
public MigrationQueueBuilder With(IConfigurationMigration migration)
{
_migrations.Add(migration);
return this;
}
public MigrationQueue Build()
{
return new MigrationQueue(_migrations);
}
}

View File

@@ -1,3 +1,4 @@
using Nebula.Shared.Configurations;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;

View File

@@ -50,6 +50,21 @@ public class AuthService(
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", tokenCredentials.Token.Token); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", tokenCredentials.Token.Token);
using var resp = await _httpClient.SendAsync(requestMessage, cancellationService.Token); using var resp = await _httpClient.SendAsync(requestMessage, cancellationService.Token);
} }
public async Task Logout(AuthTokenCredentials tokenCredentials)
{
var authUrl = new Uri($"{tokenCredentials.AuthServer}api/auth/logout");
await restService.PostAsync<NullResponse, TokenRequest>(TokenRequest.From(tokenCredentials), authUrl, cancellationService.Token);
}
public async Task<AuthTokenCredentials> Refresh(AuthTokenCredentials tokenCredentials)
{
var authUrl = new Uri($"{tokenCredentials.AuthServer}api/auth/refresh");
var newToken = await restService.PostAsync<LoginToken, TokenRequest>(
TokenRequest.From(tokenCredentials), authUrl, cancellationService.Token);
return tokenCredentials with { Token = newToken };
}
} }
public sealed record AuthTokenCredentials(Guid UserId, LoginToken Token, string Login, string AuthServer); public sealed record AuthTokenCredentials(Guid UserId, LoginToken Token, string Login, string AuthServer);
@@ -71,3 +86,14 @@ public enum AuthenticateDenyCode
TfaInvalid = 4, TfaInvalid = 4,
AccountLocked = 5, AccountLocked = 5,
} }
public sealed record TokenRequest(string Token)
{
public static TokenRequest From(AuthTokenCredentials authTokenCredentials)
{
return new TokenRequest(authTokenCredentials.Token.Token);
}
public static TokenRequest Empty { get; } = new TokenRequest("");
}

View File

@@ -1,107 +1,13 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.DependencyInjection; using Nebula.Shared.Configurations;
using Nebula.Shared.Configurations.Migrations;
using Nebula.Shared.FileApis.Interfaces; using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services.Logging; using Nebula.Shared.Services.Logging;
using Robust.LoaderApi;
namespace Nebula.Shared.Services; namespace Nebula.Shared.Services;
public class ConVar<T>
{
internal ConfigurationService.OnConfigurationChangedDelegate<T?>? OnValueChanged;
public ConVar(string name, T? defaultValue = default)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
DefaultValue = defaultValue;
}
public string Name { get; }
public Type Type => typeof(T);
public T? DefaultValue { get; }
}
public static class ConVarBuilder
{
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
return new ConVar<T>(name, defaultValue);
}
public static ConVar<T> BuildWithMigration<T>(string name, IConfigurationMigration migration, T? defaultValue = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
ConfigurationService.AddConfigurationMigration(migration);
return new ConVar<T>(name, defaultValue);
}
}
public interface IConfigurationMigration
{
public Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler);
}
public abstract class BaseConfigurationMigration<T1,T2> : IConfigurationMigration
{
protected ConVar<T1> OldConVar;
protected ConVar<T2> NewConVar;
public BaseConfigurationMigration(string oldName, string newName)
{
OldConVar = ConVarBuilder.Build<T1>(oldName);
NewConVar = ConVarBuilder.Build<T2>(newName);
}
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler)
{
var oldValue = configurationService.GetConfigValue(OldConVar);
if(oldValue == null) return;
var newValue = await Migrate(serviceProvider, oldValue, loadingHandler);
configurationService.SetConfigValue(NewConVar, newValue);
configurationService.ClearConfigValue(OldConVar);
}
protected abstract Task<T2> Migrate(IServiceProvider serviceProvider, T1 oldValue, ILoadingHandler loadingHandler);
}
public class MigrationQueue(List<IConfigurationMigration> migrations) : IConfigurationMigration
{
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider , ILoadingHandler loadingHandler)
{
foreach (var migration in migrations)
{
await migration.DoMigrate(configurationService, serviceProvider, loadingHandler);
}
}
}
public class MigrationQueueBuilder
{
public static MigrationQueueBuilder Instance => new();
private readonly List<IConfigurationMigration> _migrations = [];
public MigrationQueueBuilder With(IConfigurationMigration migration)
{
_migrations.Add(migration);
return this;
}
public MigrationQueue Build()
{
return new MigrationQueue(_migrations);
}
}
[ServiceRegister] [ServiceRegister]
public class ConfigurationService public class ConfigurationService
{ {
@@ -142,15 +48,22 @@ public class ConfigurationService
}); });
} }
public ConfigChangeSubscriberDisposable<T> SubscribeVarChanged<T>(ConVar<T> convar, OnConfigurationChangedDelegate<T?> @delegate, bool invokeNow = false) public ConVarObserver<T> SubscribeVarChanged<T>(ConVar<T> convar, OnConfigurationChangedDelegate<T?> @delegate, bool invokeNow = false)
{ {
convar.OnValueChanged += @delegate; convar.OnValueChanged += @delegate;
if (invokeNow) if (invokeNow)
{ {
@delegate(GetConfigValue(convar)); @delegate(GetConfigValue(convar));
} }
return new ConfigChangeSubscriberDisposable<T>(convar, @delegate); var delegation = SubscribeVarChanged<T>(convar);
delegation.PropertyChanged += (_, _) => @delegate(delegation.Value);
return delegation;
}
public ConVarObserver<T> SubscribeVarChanged<T>(ConVar<T> convar)
{
return new ConVarObserver<T>(convar, this);
} }
public T? GetConfigValue<T>(ConVar<T> conVar) public T? GetConfigValue<T>(ConVar<T> conVar)
@@ -252,20 +165,4 @@ public class ConfigurationService
{ {
return $"{conVar.Name}.json"; return $"{conVar.Name}.json";
} }
}
public sealed class ConfigChangeSubscriberDisposable<T> : IDisposable
{
private readonly ConVar<T> _convar;
private readonly ConfigurationService.OnConfigurationChangedDelegate<T> _delegate;
public ConfigChangeSubscriberDisposable(ConVar<T> convar, ConfigurationService.OnConfigurationChangedDelegate<T> @delegate)
{
_convar = convar;
_delegate = @delegate;
}
public void Dispose()
{
_convar.OnValueChanged -= _delegate;
}
} }

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Nebula.Shared.Configurations;
using Nebula.Shared.FileApis; using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces; using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models; using Nebula.Shared.Models;

View File

@@ -46,8 +46,7 @@ public class RestService
return defaultValue; return defaultValue;
} }
} }
[Pure]
public async Task<K> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull public async Task<K> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull
{ {
var json = JsonSerializer.Serialize(information, _serializerOptions); var json = JsonSerializer.Serialize(information, _serializerOptions);
@@ -57,11 +56,11 @@ public class RestService
} }
[Pure] [Pure]
public async Task<T> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull public async Task<T> PostAsync<T>(Stream stream, string fileName, Uri uri, CancellationToken cancellationToken) where T : notnull
{ {
using var multipartFormContent = using var multipartFormContent =
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
multipartFormContent.Add(new StreamContent(stream), "formFile", "image.png"); multipartFormContent.Add(new StreamContent(stream), "formFile", fileName);
var response = await _client.PostAsync(uri, multipartFormContent, cancellationToken); var response = await _client.PostAsync(uri, multipartFormContent, cancellationToken);
return await ReadResult<T>(response, cancellationToken, uri); return await ReadResult<T>(response, cancellationToken, uri);
} }
@@ -76,9 +75,12 @@ public class RestService
[Pure] [Pure]
private async Task<T> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken, Uri uri) where T : notnull private async Task<T> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken, Uri uri) where T : notnull
{ {
var content = await response.Content.ReadAsStringAsync(cancellationToken); if (typeof(T) == typeof(NullResponse) && new NullResponse() is T nullResponse)
{
return nullResponse;
}
if (typeof(T) == typeof(string) && content is T t) if (typeof(T) == typeof(string) && await response.Content.ReadAsStringAsync(cancellationToken) is T t)
return t; return t;
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@@ -90,6 +92,10 @@ public class RestService
} }
} }
public sealed class NullResponse
{
}
public sealed class RestRequestException(HttpContent content, HttpStatusCode statusCode, string message) : Exception(message) public sealed class RestRequestException(HttpContent content, HttpStatusCode statusCode, string message) : Exception(message)
{ {
public HttpStatusCode StatusCode { get; } = statusCode; public HttpStatusCode StatusCode { get; } = statusCode;

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nebula.Shared.Configurations;
using Nebula.Shared.Services; using Nebula.Shared.Services;
namespace Nebula.UnitTest.NebulaSharedTests; namespace Nebula.UnitTest.NebulaSharedTests;

View File

@@ -45,6 +45,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMetrics_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb07ddb833489431aae882d295a4e94797e00_003F0f_003Fc9e3d448_003FMetrics_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMetrics_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb07ddb833489431aae882d295a4e94797e00_003F0f_003Fc9e3d448_003FMetrics_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANativeLibrary_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88c2c65e1618f68cb5969f70dfc0986e9571015ac8d487b18d26e89c926264_003FNativeLibrary_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANativeLibrary_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88c2c65e1618f68cb5969f70dfc0986e9571015ac8d487b18d26e89c926264_003FNativeLibrary_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANativeLibrary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F49393f3cda2f9a5c2fa811fc9179dcbaf5bd94d9dc8afc76aaff2bc23287f3_003FNativeLibrary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANativeLibrary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F49393f3cda2f9a5c2fa811fc9179dcbaf5bd94d9dc8afc76aaff2bc23287f3_003FNativeLibrary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5acc345db3c207bc9d886a36ff14867ef8d65557432172c2a42f19aeac04d1b_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -54,6 +55,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APerfCounterCollector_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb07ddb833489431aae882d295a4e94797e00_003F4f_003F4c0b90e8_003FPerfCounterCollector_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APerfCounterCollector_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb07ddb833489431aae882d295a4e94797e00_003F4f_003F4c0b90e8_003FPerfCounterCollector_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProcessStartInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc5ffb8c166be164bc221db4c64e826a1e8ff54f2f1c9ee8e7f9cfabce707fa4_003FProcessStartInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProcessStartInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc5ffb8c166be164bc221db4c64e826a1e8ff54f2f1c9ee8e7f9cfabce707fa4_003FProcessStartInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APropertyInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F408236be4b0703755f3ed96daaae245919a792d65ce5eaa672d9fa945b1f_003FPropertyInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APropertyInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F408236be4b0703755f3ed96daaae245919a792d65ce5eaa672d9fa945b1f_003FPropertyInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARelayCommand_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F20c0f49b8854743afaecc2f359655fdbfc6c5264f49e9eb333686e85a87bf_003FRelayCommand_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AScrollBar_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fda7bce95d5f888176a5f93c8965e402ca33cba794ac7e7aa776363c664488d_003FScrollBar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AScrollBar_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fda7bce95d5f888176a5f93c8965e402ca33cba794ac7e7aa776363c664488d_003FScrollBar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProviderServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4f1fdec7cbfe4433a7ec3a6d1bd0e54210118_003F04_003Fe2f5322d_003FServiceProviderServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProviderServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4f1fdec7cbfe4433a7ec3a6d1bd0e54210118_003F04_003Fe2f5322d_003FServiceProviderServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>