using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.Configurations; 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; using Nebula.Shared.Utils; using Nebula.Shared.ViewHelper; namespace Nebula.Launcher.ViewModels.Pages; [ViewModelRegister(typeof(AccountInfoView))] [ConstructGenerator] public partial class AccountInfoViewModel : ViewModelBase { [ObservableProperty] private bool _authMenuExpand; [ObservableProperty] private bool _authUrlConfigExpand; [ObservableProperty] private int _authViewSpan = 1; [ObservableProperty] private string _currentAuthServer = string.Empty; [ObservableProperty] private string _currentLogin = string.Empty; [ObservableProperty] private string _currentPassword = string.Empty; [ObservableProperty] private bool _isLogged; [ObservableProperty] private bool _doRetryAuth; [ObservableProperty] private AuthServerCredentials _authItemSelect; [ObservableProperty] private string _authServerName; private bool _isProfilesEmpty; [GenerateProperty] private PopupMessageService PopupMessageService { get; } [GenerateProperty] private ConfigurationService ConfigurationService { get; } [GenerateProperty] private DebugService DebugService { get; } [GenerateProperty] private AuthService AuthService { get; } [GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } public ObservableCollection Accounts { get; } = new(); public ObservableCollection AuthUrls { get; } = new(); public ComplexConVarBinder Credentials { get; private set; } private ILogger _logger; //Design think protected override void InitialiseInDesignMode() { AuthUrls.Add(new AuthServerCredentials("Test",["example.com","variant.lab"])); AddAccount(new ProfileAuthCredentials("Binka", "","example.com")); AddAccount(new ProfileAuthCredentials("Vilka","", "variant.lab")); } //Real think protected override void Initialise() { _logger = DebugService.GetLogger(this); Credentials = new AuthTokenCredentialsVar(this); Task.Run(ReadAuthConfig); Credentials.Value = Credentials.Value; } public void DoAuth(string? code = null) { var message = ViewHelperService.GetViewModel(); message.InfoText = LocalizationService.GetString("auth-processing"); message.IsInfoClosable = false; PopupMessageService.Popup(message); var serverCandidates = new List(); if (string.IsNullOrWhiteSpace(CurrentAuthServer)) serverCandidates.AddRange(AuthItemSelect.Servers); else serverCandidates.Add(CurrentAuthServer); Task.Run(async () => { Exception? exception = null; foreach (var server in serverCandidates) { try { await CatchAuthError(async() => { Credentials.Value = await AuthService.Auth(CurrentLogin, CurrentPassword, server, code); }, ()=> message.Dispose()); break; } catch (Exception ex) { exception = new Exception(LocalizationService.GetString("auth-error"), ex); } } message.Dispose(); if (exception != null) { PopupMessageService.Popup(new Exception("Error while auth", exception)); } }); } private async Task CatchAuthError(Func a, Action? onError) { DoRetryAuth = false; try { await a(); } catch (AuthException e) { onError?.Invoke(); switch (e.Error.Code) { case AuthenticateDenyCode.TfaRequired: case AuthenticateDenyCode.TfaInvalid: var p = ViewHelperService.GetViewModel(); PopupMessageService.Popup(p); _logger.Log("TFA required"); break; case AuthenticateDenyCode.InvalidCredentials: PopupError(LocalizationService.GetString("auth-invalid-credentials"), e); break; case AuthenticateDenyCode.AccountLocked: PopupError(LocalizationService.GetString("auth-account-locked"), e); break; case AuthenticateDenyCode.AccountUnconfirmed: PopupError(LocalizationService.GetString("auth-account-unconfirmed"), e); break; case AuthenticateDenyCode.None: PopupError(LocalizationService.GetString("auth-none"),e); break; default: PopupError(LocalizationService.GetString("auth-error-fuck"), e); break; } } catch (HttpRequestException e) { onError?.Invoke(); switch (e.HttpRequestError) { case HttpRequestError.ConnectionError: PopupError(LocalizationService.GetString("auth-connection-error"), e); DoRetryAuth = true; break; case HttpRequestError.NameResolutionError: PopupError(LocalizationService.GetString("auth-name-resolution-error"), e); DoRetryAuth = true; break; case HttpRequestError.SecureConnectionError: PopupError(LocalizationService.GetString("auth-secure-error"), e); DoRetryAuth = true; break; case HttpRequestError.UserAuthenticationError: PopupError(LocalizationService.GetString("auth-user-authentication-error"), e); break; case HttpRequestError.Unknown: PopupError(LocalizationService.GetString("auth-unknown"), e); break; case HttpRequestError.HttpProtocolError: PopupError(LocalizationService.GetString("auth-http-protocol-error"), e); break; case HttpRequestError.ExtendedConnectNotSupported: PopupError(LocalizationService.GetString("auth-extended-connect-not-support"), e); break; case HttpRequestError.VersionNegotiationError: PopupError(LocalizationService.GetString("auth-version-negotiation-error"), e); break; case HttpRequestError.ProxyTunnelError: PopupError(LocalizationService.GetString("auth-proxy-tunnel-error"), e); break; case HttpRequestError.InvalidResponse: PopupError(LocalizationService.GetString("auth-invalid-response"), e); break; case HttpRequestError.ResponseEnded: PopupError(LocalizationService.GetString("auth-response-ended"), e); break; case HttpRequestError.ConfigurationLimitExceeded: PopupError(LocalizationService.GetString("auth-configuration-limit-exceeded"), e); break; default: var authError = new Exception(LocalizationService.GetString("auth-error"), e); _logger.Error(authError); PopupMessageService.Popup(authError); break; } } } public void Logout() { Credentials.Value = null; CurrentAuthServer = ""; } public string GetServerAuthName(string? url) { if (url is null) return ""; return AuthUrls.FirstOrDefault(p => p.Servers.Contains(url))?.Name ?? "CustomAuth"; } private void UpdateAuthMenu() { if (AuthMenuExpand || _isProfilesEmpty) AuthViewSpan = 2; else AuthViewSpan = 1; } private void AddAccount(ProfileAuthCredentials credentials) { var onDelete = new DelegateCommand(OnDeleteProfile); var onSelect = new DelegateCommand((p) => { CurrentLogin = p.Credentials.Login; CurrentPassword = p.Credentials.Password; CurrentAuthServer = p.Credentials.AuthServer; DoAuth(); }); var serverName = GetServerAuthName(credentials.AuthServer); var alpm = new ProfileEntry( credentials, serverName, onSelect, onDelete); onDelete.TRef.Value = alpm; onSelect.TRef.Value = alpm; Accounts.Add(alpm); } private async Task ReadAuthConfig() { var message = ViewHelperService.GetViewModel(); message.InfoText = LocalizationService.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 profileRaw in ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!) { _logger.Log($"Decrypting profile..."); try { var decoded = await CryptographicStore.Decrypt(profileRaw, CryptographicStore.GetComputerKey()); _logger.Log($"Decrypted profile: {decoded.Login}"); profileCandidates.Add(profileRaw); AddAccount(decoded); } catch (Exception e) { _logger.Error("Error while decrypting profile"); _logger.Error(e); } } ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, profileCandidates.ToArray()); if (Accounts.Count == 0) UpdateAuthMenu(); message.Dispose(); } public void DoCurrentAuth() { DoAuth(); } private async Task CheckOrRenewToken(AuthTokenCredentials? authTokenCredentials) { if(authTokenCredentials is null) return null; var daysLeft = (int)(authTokenCredentials.Token.ExpireTime - DateTime.Now).TotalDays; if(daysLeft >= 4) { _logger.Log("Token " + authTokenCredentials.Login + " is active, "+daysLeft+" days left, undo renewing!"); return authTokenCredentials; } try { _logger.Log($"Renewing token for {authTokenCredentials.Login}"); return await ExceptionHelper.TryRun(() => AuthService.Refresh(authTokenCredentials), 3, (attempt, e) => { _logger.Error(new Exception("Error while renewing, attempts: " + attempt, e)); }); } catch (AuthTokenExpiredException e) { _logger.Error(e); return null; } catch (Exception e) { var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), e); _logger.Error(unexpectedError); return authTokenCredentials; } } public void OnSaveProfile() { if(Credentials.Value is null || string.IsNullOrEmpty(CurrentPassword)) return; AddAccount(new ProfileAuthCredentials(CurrentLogin, CurrentPassword, Credentials.Value.AuthServer)); _isProfilesEmpty = Accounts.Count == 0; UpdateAuthMenu(); DirtyProfile(); } private void OnDeleteProfile(ProfileEntry account) { Accounts.Remove(account); _isProfilesEmpty = Accounts.Count == 0; UpdateAuthMenu(); DirtyProfile(); } private void PopupError(string message, Exception e) { message = LocalizationService.GetString("auth-error-occured") + message; _logger.Error(new Exception(message, e)); var messageView = ViewHelperService.GetViewModel(); messageView.InfoText = message; messageView.IsInfoClosable = true; PopupMessageService.Popup(messageView); } public void OnExpandAuthUrl() { AuthUrlConfigExpand = !AuthUrlConfigExpand; } public void OnExpandAuthView() { AuthMenuExpand = !AuthMenuExpand; UpdateAuthMenu(); } private void DirtyProfile() { ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, Accounts.Select(a => CryptographicStore.Encrypt(a.Credentials, CryptographicStore.GetComputerKey())).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"); return null; } var message = accountInfoViewModel.ViewHelperService.GetViewModel(); message.InfoText = LocalizationService.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 accountInfoViewModel.CheckOrRenewToken(currProfile); if (currProfile is null) { message.Dispose(); accountInfoViewModel._logger.Log("profile credentials update required!"); accountInfoViewModel.PopupMessageService.Popup("profile credentials update required!"); accountInfoViewModel.IsLogged = false; return null; } 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(LocalizationService.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.AuthServerName = accountInfoViewModel.GetServerAuthName(currProfile.AuthServer); return currProfile; } } }