From 6a6bb4f27c1f78d94ec5f5a87b4303b42311cddb Mon Sep 17 00:00:00 2001 From: Cinka Date: Wed, 6 Aug 2025 21:29:00 +0300 Subject: [PATCH] - fix: auth logic part 1 --- Nebula.Launcher/ColorUtils.cs | 20 ++ Nebula.Launcher/Converters/TypeConverters.cs | 5 + Nebula.Launcher/LauncherConVar.cs | 2 + .../Models/Auth/ProfileAuthCredentials.cs | 1 - .../GameProcessStartInfoProvider.cs | 2 +- Nebula.Launcher/ViewModels/MainViewModel.cs | 4 +- .../ViewModels/Pages/AccountInfoViewModel.cs | 294 ++++++++++-------- .../Pages/ConfigurationViewModel.cs | 1 + .../ViewModels/ServerEntryModelView.cs | 2 +- .../Views/Pages/AccountInfoView.axaml | 12 +- .../ConfigMigrations/ProfileMigration.cs | 1 + .../Configurations/ComplexConVarBinder.cs | 70 +++++ Nebula.Shared/Configurations/ConVar.cs | 18 ++ Nebula.Shared/Configurations/ConVarBuilder.cs | 25 ++ .../Configurations/ConVarObserver.cs | 68 ++++ .../Migrations/BaseConfigurationMigration.cs | 28 ++ .../Migrations/IConfigurationMigration.cs | 9 + .../Migrations/MigrationQueue.cs | 15 + .../Migrations/MigrationQueueBuilder.cs | 19 ++ Nebula.Shared/CurrentConVar.cs | 1 + Nebula.Shared/Services/AuthService.cs | 26 ++ .../Services/ConfigurationService.cs | 127 +------- Nebula.Shared/Services/EngineService.cs | 1 + Nebula.Shared/Services/RestService.cs | 18 +- .../ConfigurationServiceTests.cs | 1 + Nebula.sln.DotSettings.user | 2 + 26 files changed, 513 insertions(+), 259 deletions(-) create mode 100644 Nebula.Launcher/ColorUtils.cs create mode 100644 Nebula.Shared/Configurations/ComplexConVarBinder.cs create mode 100644 Nebula.Shared/Configurations/ConVar.cs create mode 100644 Nebula.Shared/Configurations/ConVarBuilder.cs create mode 100644 Nebula.Shared/Configurations/ConVarObserver.cs create mode 100644 Nebula.Shared/Configurations/Migrations/BaseConfigurationMigration.cs create mode 100644 Nebula.Shared/Configurations/Migrations/IConfigurationMigration.cs create mode 100644 Nebula.Shared/Configurations/Migrations/MigrationQueue.cs create mode 100644 Nebula.Shared/Configurations/Migrations/MigrationQueueBuilder.cs diff --git a/Nebula.Launcher/ColorUtils.cs b/Nebula.Launcher/ColorUtils.cs new file mode 100644 index 0000000..8b1a930 --- /dev/null +++ b/Nebula.Launcher/ColorUtils.cs @@ -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); + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Converters/TypeConverters.cs b/Nebula.Launcher/Converters/TypeConverters.cs index 91c2617..6fdf9c1 100644 --- a/Nebula.Launcher/Converters/TypeConverters.cs +++ b/Nebula.Launcher/Converters/TypeConverters.cs @@ -2,6 +2,8 @@ using System; using Avalonia.Data.Converters; using Avalonia.Media; using Avalonia.Platform; +using Nebula.Launcher.ViewModels.Pages; +using Color = System.Drawing.Color; namespace Nebula.Launcher.Converters; @@ -20,4 +22,7 @@ public static class TypeConverters if (iconKey == null) return null; return new Avalonia.Media.Imaging.Bitmap(AssetLoader.Open(new Uri($"avares://Nebula.Launcher/Assets/error_presentation/{iconKey}.png"))); }); + + public static FuncValueConverter NameColorRepresentation { get; } = + new(ColorUtils.GetColorFromString); } \ No newline at end of file diff --git a/Nebula.Launcher/LauncherConVar.cs b/Nebula.Launcher/LauncherConVar.cs index f2174d9..d2a2644 100644 --- a/Nebula.Launcher/LauncherConVar.cs +++ b/Nebula.Launcher/LauncherConVar.cs @@ -3,6 +3,8 @@ using System.Globalization; using Nebula.Launcher.Models; using Nebula.Launcher.Models.Auth; using Nebula.Shared.ConfigMigrations; +using Nebula.Shared.Configurations; +using Nebula.Shared.Configurations.Migrations; using Nebula.Shared.Services; namespace Nebula.Launcher; diff --git a/Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs b/Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs index 9512ad7..ca0405e 100644 --- a/Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs +++ b/Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs @@ -8,6 +8,5 @@ namespace Nebula.Launcher.Models.Auth; public sealed record ProfileAuthCredentials( AuthTokenCredentials Credentials, string AuthName, - Color Color, [property: JsonIgnore] ICommand OnSelect = default!, [property: JsonIgnore] ICommand OnDelete = default!); \ No newline at end of file diff --git a/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs b/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs index fd25dd9..1308e5e 100644 --- a/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs +++ b/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs @@ -35,7 +35,7 @@ public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverS { var baseStart = await base.GetProcessStartInfo(); - var authProv = accountInfoViewModel.Credentials; + var authProv = accountInfoViewModel.Credentials.Value; if(authProv is null) throw new Exception("Client is without selected auth"); diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index 098b3b9..c61c72b 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -41,12 +41,12 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private bool _popup; [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", new Dictionary { - { "login", AccountInfoViewModel.Credentials?.Login ?? "" }, + { "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" }, { "auth_server", AccountInfoViewModel.CurrentAuthServerName} }); diff --git a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs index b6c3c6d..7eb8e75 100644 --- a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs @@ -3,16 +3,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; -using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; -using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using Nebula.Launcher.Models.Auth; using Nebula.Launcher.Services; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views.Pages; +using Nebula.Shared.Configurations; using Nebula.Shared.Models.Auth; using Nebula.Shared.Services; using Nebula.Shared.Services.Logging; @@ -33,7 +30,6 @@ public partial class AccountInfoViewModel : ViewModelBase [ObservableProperty] private string _currentPassword = string.Empty; [ObservableProperty] private bool _isLogged; [ObservableProperty] private bool _doRetryAuth; - [ObservableProperty] private AuthTokenCredentials? _credentials; [ObservableProperty] private AuthServerCredentials _authItemSelect; private bool _isProfilesEmpty; @@ -45,15 +41,12 @@ public partial class AccountInfoViewModel : ViewModelBase public ObservableCollection Accounts { get; } = new(); public ObservableCollection AuthUrls { get; } = new(); - public string CurrentAuthServerName => GetServerAuthName(Credentials); + public string CurrentAuthServerName => GetServerAuthName(Credentials.Value); + + public ComplexConVarBinder Credentials; private ILogger _logger; - partial void OnCredentialsChanged(AuthTokenCredentials? value) - { - OnPropertyChanged(nameof(CurrentAuthServerName)); - } - //Design think 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", "")); } - + //Real think protected override void Initialise() { _logger = DebugService.GetLogger(this); + Credentials = new AuthTokenCredentialsVar(this); Task.Run(ReadAuthConfig); } - - public async void AuthByProfile(ProfileAuthCredentials credentials) - { - var message = ViewHelperService.GetViewModel(); - 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) { var message = ViewHelperService.GetViewModel(); @@ -114,54 +81,33 @@ public partial class AccountInfoViewModel : ViewModelBase Task.Run(async () => { Exception? exception = null; + AuthTokenCredentials? credentials = null; + foreach (var server in serverCandidates) { try { - await CatchAuthError(async () => await TryAuth(CurrentLogin, CurrentPassword, server, code), ()=> message.Dispose()); + credentials = await AuthService.Auth(CurrentLogin, CurrentPassword, server, code); break; } catch (Exception ex) { - var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex); - _logger.Error(unexpectedError); - PopupMessageService.Popup(unexpectedError); + exception = new Exception(LocalisationService.GetString("auth-error"), ex); } } message.Dispose(); - if (!IsLogged) + if (credentials is null) { 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 a, Action? onError) { DoRetryAuth = false; @@ -185,8 +131,18 @@ public partial class AccountInfoViewModel : ViewModelBase case AuthenticateDenyCode.InvalidCredentials: PopupError(LocalisationService.GetString("auth-invalid-credentials"), e); 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: - throw; + PopupError(LocalisationService.GetString("auth-error-fuck"), e); + break; } } catch (HttpRequestException e) @@ -198,17 +154,41 @@ public partial class AccountInfoViewModel : ViewModelBase PopupError(LocalisationService.GetString("auth-connection-error"), e); DoRetryAuth = true; break; - case HttpRequestError.NameResolutionError: PopupError(LocalisationService.GetString("auth-name-resolution-error"), e); DoRetryAuth = true; break; - case HttpRequestError.SecureConnectionError: PopupError(LocalisationService.GetString("auth-secure-error"), e); DoRetryAuth = true; 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: var authError = new Exception(LocalisationService.GetString("auth-error"), e); _logger.Error(authError); @@ -225,8 +205,7 @@ public partial class AccountInfoViewModel : ViewModelBase public void Logout() { - IsLogged = false; - Credentials = null; + Credentials.Value = null; CurrentAuthServer = ""; } @@ -247,14 +226,13 @@ public partial class AccountInfoViewModel : ViewModelBase private void AddAccount(AuthTokenCredentials credentials) { var onDelete = new DelegateCommand(OnDeleteProfile); - var onSelect = new DelegateCommand(AuthByProfile); + var onSelect = new DelegateCommand((p) => Credentials.Value = p.Credentials); var serverName = GetServerAuthName(credentials); var alpm = new ProfileAuthCredentials( credentials, serverName, - ColorUtils.GetColorFromString(credentials.AuthServer), onSelect, onDelete); @@ -264,62 +242,77 @@ public partial class AccountInfoViewModel : ViewModelBase Accounts.Add(alpm); } - private void ReadAuthConfig() + private async void ReadAuthConfig() { var message = ViewHelperService.GetViewModel(); message.InfoText = LocalisationService.GetString("auth-config-read"); message.IsInfoClosable = false; PopupMessageService.Popup(message); + _logger.Log("Reading auth config"); + AuthUrls.Clear(); var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!; foreach (var url in authUrls) AuthUrls.Add(url); if(authUrls.Length > 0) AuthItemSelect = authUrls[0]; + var profileCandidates = new List(); + foreach (var profile in 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(); message.Dispose(); - - DoCurrentAuth(); } - public async void DoCurrentAuth() + public void DoCurrentAuth() { - var message = ViewHelperService.GetViewModel(); - 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(); + DoAuth(); } - [RelayCommand] - private void OnSaveProfile() + private async Task CheckOrRenewToken(AuthTokenCredentials? authTokenCredentials) { - 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; UpdateAuthMenu(); DirtyProfile(); @@ -343,15 +336,13 @@ public partial class AccountInfoViewModel : ViewModelBase messageView.IsInfoClosable = true; PopupMessageService.Popup(messageView); } - - [RelayCommand] - private void OnExpandAuthUrl() + + public void OnExpandAuthUrl() { AuthUrlConfigExpand = !AuthUrlConfigExpand; } - - [RelayCommand] - private void OnExpandAuthView() + + public void OnExpandAuthView() { AuthMenuExpand = !AuthMenuExpand; UpdateAuthMenu(); @@ -362,18 +353,65 @@ public partial class AccountInfoViewModel : ViewModelBase ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, Accounts.Select(a => a.Credentials).ToArray()); } + + public sealed class AuthTokenCredentialsVar(AccountInfoViewModel accountInfoViewModel) + : ComplexConVarBinder( + accountInfoViewModel.ConfigurationService.SubscribeVarChanged(LauncherConVar.AuthCurrent)) + { + protected override async Task 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(); + 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); - } -} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs index 9da50d1..6064876 100644 --- a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs @@ -15,6 +15,7 @@ using Nebula.Launcher.Services; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views.Pages; using Nebula.Shared; +using Nebula.Shared.Configurations; using Nebula.Shared.Services; using Nebula.Shared.ViewHelper; diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index 84a7d78..ab751a7 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -174,7 +174,7 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis private async Task RunInstanceAsync(bool ignoreLoginCredentials = false) { - if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials is null) + if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials.Value is null) { var warningContext = ViewHelperService.GetViewModel() .WithServerEntry(this); diff --git a/Nebula.Launcher/Views/Pages/AccountInfoView.axaml b/Nebula.Launcher/Views/Pages/AccountInfoView.axaml index 2ce936b..cdfdfee 100644 --- a/Nebula.Launcher/Views/Pages/AccountInfoView.axaml +++ b/Nebula.Launcher/Views/Pages/AccountInfoView.axaml @@ -10,7 +10,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages" 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"> @@ -46,7 +47,8 @@ Margin="5,5,5,0"> - + @@ -138,7 +140,7 @@ - @@ -173,7 +175,7 @@ - - diff --git a/Nebula.Shared/ConfigMigrations/ProfileMigration.cs b/Nebula.Shared/ConfigMigrations/ProfileMigration.cs index 63cba0f..8ac9ffe 100644 --- a/Nebula.Shared/ConfigMigrations/ProfileMigration.cs +++ b/Nebula.Shared/ConfigMigrations/ProfileMigration.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Nebula.Shared.Configurations.Migrations; using Nebula.Shared.Models; using Nebula.Shared.Services; diff --git a/Nebula.Shared/Configurations/ComplexConVarBinder.cs b/Nebula.Shared/Configurations/ComplexConVarBinder.cs new file mode 100644 index 0000000..4711488 --- /dev/null +++ b/Nebula.Shared/Configurations/ComplexConVarBinder.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Nebula.Shared.Configurations; + +public abstract class ComplexConVarBinder : INotifyPropertyChanged, INotifyPropertyChanging +{ + private readonly ConVarObserver _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 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 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; +} diff --git a/Nebula.Shared/Configurations/ConVar.cs b/Nebula.Shared/Configurations/ConVar.cs new file mode 100644 index 0000000..ab0d221 --- /dev/null +++ b/Nebula.Shared/Configurations/ConVar.cs @@ -0,0 +1,18 @@ +using Nebula.Shared.Services; + +namespace Nebula.Shared.Configurations; + +public class ConVar +{ + internal ConfigurationService.OnConfigurationChangedDelegate? 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; } +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/ConVarBuilder.cs b/Nebula.Shared/Configurations/ConVarBuilder.cs new file mode 100644 index 0000000..442f031 --- /dev/null +++ b/Nebula.Shared/Configurations/ConVarBuilder.cs @@ -0,0 +1,25 @@ +using Nebula.Shared.Configurations.Migrations; +using Nebula.Shared.Services; + +namespace Nebula.Shared.Configurations; + +public static class ConVarBuilder +{ + public static ConVar Build(string name, T? defaultValue = default) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name)); + + return new ConVar(name, defaultValue); + } + + public static ConVar BuildWithMigration(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(name, defaultValue); + } +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/ConVarObserver.cs b/Nebula.Shared/Configurations/ConVarObserver.cs new file mode 100644 index 0000000..282850f --- /dev/null +++ b/Nebula.Shared/Configurations/ConVarObserver.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Nebula.Shared.Services; + +namespace Nebula.Shared.Configurations; + +public sealed class ConVarObserver : IDisposable, INotifyPropertyChanged, INotifyPropertyChanging +{ + private readonly ConVar _convar; + private readonly ConfigurationService _configurationService; + + private T? _value; + private ConfigurationService.OnConfigurationChangedDelegate _delegate; + + public T? Value + { + get => _value; + set => _configurationService.SetConfigValue(_convar, value); + } + + public ConVarObserver(ConVar 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 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)); + } + +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/Migrations/BaseConfigurationMigration.cs b/Nebula.Shared/Configurations/Migrations/BaseConfigurationMigration.cs new file mode 100644 index 0000000..6969bbc --- /dev/null +++ b/Nebula.Shared/Configurations/Migrations/BaseConfigurationMigration.cs @@ -0,0 +1,28 @@ +using Nebula.Shared.Models; +using Nebula.Shared.Services; + +namespace Nebula.Shared.Configurations.Migrations; + +public abstract class BaseConfigurationMigration : IConfigurationMigration +{ + protected ConVar OldConVar; + protected ConVar NewConVar; + + public BaseConfigurationMigration(string oldName, string newName) + { + OldConVar = ConVarBuilder.Build(oldName); + NewConVar = ConVarBuilder.Build(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 Migrate(IServiceProvider serviceProvider, T1 oldValue, ILoadingHandler loadingHandler); +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/Migrations/IConfigurationMigration.cs b/Nebula.Shared/Configurations/Migrations/IConfigurationMigration.cs new file mode 100644 index 0000000..8c5132f --- /dev/null +++ b/Nebula.Shared/Configurations/Migrations/IConfigurationMigration.cs @@ -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); +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/Migrations/MigrationQueue.cs b/Nebula.Shared/Configurations/Migrations/MigrationQueue.cs new file mode 100644 index 0000000..933e799 --- /dev/null +++ b/Nebula.Shared/Configurations/Migrations/MigrationQueue.cs @@ -0,0 +1,15 @@ +using Nebula.Shared.Models; +using Nebula.Shared.Services; + +namespace Nebula.Shared.Configurations.Migrations; + +public class MigrationQueue(List migrations) : IConfigurationMigration +{ + public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider , ILoadingHandler loadingHandler) + { + foreach (var migration in migrations) + { + await migration.DoMigrate(configurationService, serviceProvider, loadingHandler); + } + } +} \ No newline at end of file diff --git a/Nebula.Shared/Configurations/Migrations/MigrationQueueBuilder.cs b/Nebula.Shared/Configurations/Migrations/MigrationQueueBuilder.cs new file mode 100644 index 0000000..1de0213 --- /dev/null +++ b/Nebula.Shared/Configurations/Migrations/MigrationQueueBuilder.cs @@ -0,0 +1,19 @@ +namespace Nebula.Shared.Configurations.Migrations; + +public class MigrationQueueBuilder +{ + public static MigrationQueueBuilder Instance => new(); + + private readonly List _migrations = []; + + public MigrationQueueBuilder With(IConfigurationMigration migration) + { + _migrations.Add(migration); + return this; + } + + public MigrationQueue Build() + { + return new MigrationQueue(_migrations); + } +} \ No newline at end of file diff --git a/Nebula.Shared/CurrentConVar.cs b/Nebula.Shared/CurrentConVar.cs index 2baa43c..c1124ec 100644 --- a/Nebula.Shared/CurrentConVar.cs +++ b/Nebula.Shared/CurrentConVar.cs @@ -1,3 +1,4 @@ +using Nebula.Shared.Configurations; using Nebula.Shared.Models; using Nebula.Shared.Services; diff --git a/Nebula.Shared/Services/AuthService.cs b/Nebula.Shared/Services/AuthService.cs index 8332f7e..fef64c6 100644 --- a/Nebula.Shared/Services/AuthService.cs +++ b/Nebula.Shared/Services/AuthService.cs @@ -50,6 +50,21 @@ public class AuthService( requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", tokenCredentials.Token.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(TokenRequest.From(tokenCredentials), authUrl, cancellationService.Token); + } + + public async Task Refresh(AuthTokenCredentials tokenCredentials) + { + var authUrl = new Uri($"{tokenCredentials.AuthServer}api/auth/refresh"); + var newToken = await restService.PostAsync( + TokenRequest.From(tokenCredentials), authUrl, cancellationService.Token); + + return tokenCredentials with { Token = newToken }; + } } public sealed record AuthTokenCredentials(Guid UserId, LoginToken Token, string Login, string AuthServer); @@ -71,3 +86,14 @@ public enum AuthenticateDenyCode TfaInvalid = 4, 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(""); + +} \ No newline at end of file diff --git a/Nebula.Shared/Services/ConfigurationService.cs b/Nebula.Shared/Services/ConfigurationService.cs index 72d130d..cb82dc8 100644 --- a/Nebula.Shared/Services/ConfigurationService.cs +++ b/Nebula.Shared/Services/ConfigurationService.cs @@ -1,107 +1,13 @@ using System.Diagnostics.CodeAnalysis; 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.Models; using Nebula.Shared.Services.Logging; -using Robust.LoaderApi; namespace Nebula.Shared.Services; -public class ConVar -{ - internal ConfigurationService.OnConfigurationChangedDelegate? 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 Build(string name, T? defaultValue = default) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name)); - - return new ConVar(name, defaultValue); - } - - public static ConVar BuildWithMigration(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(name, defaultValue); - } -} - -public interface IConfigurationMigration -{ - public Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler); -} - -public abstract class BaseConfigurationMigration : IConfigurationMigration -{ - protected ConVar OldConVar; - protected ConVar NewConVar; - - public BaseConfigurationMigration(string oldName, string newName) - { - OldConVar = ConVarBuilder.Build(oldName); - NewConVar = ConVarBuilder.Build(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 Migrate(IServiceProvider serviceProvider, T1 oldValue, ILoadingHandler loadingHandler); -} - -public class MigrationQueue(List 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 _migrations = []; - - public MigrationQueueBuilder With(IConfigurationMigration migration) - { - _migrations.Add(migration); - return this; - } - - public MigrationQueue Build() - { - return new MigrationQueue(_migrations); - } -} - [ServiceRegister] public class ConfigurationService { @@ -142,15 +48,22 @@ public class ConfigurationService }); } - public ConfigChangeSubscriberDisposable SubscribeVarChanged(ConVar convar, OnConfigurationChangedDelegate @delegate, bool invokeNow = false) + public ConVarObserver SubscribeVarChanged(ConVar convar, OnConfigurationChangedDelegate @delegate, bool invokeNow = false) { convar.OnValueChanged += @delegate; if (invokeNow) { @delegate(GetConfigValue(convar)); } - - return new ConfigChangeSubscriberDisposable(convar, @delegate); + + var delegation = SubscribeVarChanged(convar); + delegation.PropertyChanged += (_, _) => @delegate(delegation.Value); + return delegation; + } + + public ConVarObserver SubscribeVarChanged(ConVar convar) + { + return new ConVarObserver(convar, this); } public T? GetConfigValue(ConVar conVar) @@ -252,20 +165,4 @@ public class ConfigurationService { return $"{conVar.Name}.json"; } -} - -public sealed class ConfigChangeSubscriberDisposable : IDisposable -{ - private readonly ConVar _convar; - private readonly ConfigurationService.OnConfigurationChangedDelegate _delegate; - - public ConfigChangeSubscriberDisposable(ConVar convar, ConfigurationService.OnConfigurationChangedDelegate @delegate) - { - _convar = convar; - _delegate = @delegate; - } - public void Dispose() - { - _convar.OnValueChanged -= _delegate; - } } \ No newline at end of file diff --git a/Nebula.Shared/Services/EngineService.cs b/Nebula.Shared/Services/EngineService.cs index 8c1b61a..833f573 100644 --- a/Nebula.Shared/Services/EngineService.cs +++ b/Nebula.Shared/Services/EngineService.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Nebula.Shared.Configurations; using Nebula.Shared.FileApis; using Nebula.Shared.FileApis.Interfaces; using Nebula.Shared.Models; diff --git a/Nebula.Shared/Services/RestService.cs b/Nebula.Shared/Services/RestService.cs index adf5285..2f36b53 100644 --- a/Nebula.Shared/Services/RestService.cs +++ b/Nebula.Shared/Services/RestService.cs @@ -46,8 +46,7 @@ public class RestService return defaultValue; } } - - [Pure] + public async Task PostAsync(T information, Uri uri, CancellationToken cancellationToken) where K : notnull { var json = JsonSerializer.Serialize(information, _serializerOptions); @@ -57,11 +56,11 @@ public class RestService } [Pure] - public async Task PostAsync(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull + public async Task PostAsync(Stream stream, string fileName, Uri uri, CancellationToken cancellationToken) where T : notnull { using var multipartFormContent = 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); return await ReadResult(response, cancellationToken, uri); } @@ -76,9 +75,12 @@ public class RestService [Pure] private async Task ReadResult(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; 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 HttpStatusCode StatusCode { get; } = statusCode; diff --git a/Nebula.UnitTest/NebulaSharedTests/ConfigurationServiceTests.cs b/Nebula.UnitTest/NebulaSharedTests/ConfigurationServiceTests.cs index 9f5e541..8caa533 100644 --- a/Nebula.UnitTest/NebulaSharedTests/ConfigurationServiceTests.cs +++ b/Nebula.UnitTest/NebulaSharedTests/ConfigurationServiceTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Nebula.Shared.Configurations; using Nebula.Shared.Services; namespace Nebula.UnitTest.NebulaSharedTests; diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user index 8a0555d..e73f77b 100644 --- a/Nebula.sln.DotSettings.user +++ b/Nebula.sln.DotSettings.user @@ -45,6 +45,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -54,6 +55,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded