From cf7e81ba54bdb78bbd8bd773b32b5d4b8cd01706 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 18 Jan 2025 20:25:39 -0600 Subject: [PATCH] Detect disconnection from server, automatically go into offline mode --- .../ConnectionState.cs | 7 + .../AuthenticationService.cs | 122 +++- .../Extensions/IServiceProviderExtensions.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 5 +- .../KeepAliveService.cs | 120 ++++ .../ProfileService.cs | 130 +--- .../Authenticate/Components/LoginForm.razor | 6 +- .../Components/RegistrationForm.razor | 6 +- .../UI/Components/ConnectionStateView.razor | 15 + .../UI/Components/Footer.razor | 26 +- .../UI/Components/KeepAliveContainer.razor | 60 ++ .../UI/Components/MediaImage.razor | 4 +- .../UI/Components/ProfileButton.razor | 11 +- LANCommander.Launcher/UI/Library/Index.razor | 45 +- LANCommander.Launcher/UI/MainLayout.razor | 159 +++-- LANCommander.SDK/Client.cs | 560 +++++++++++------- 16 files changed, 844 insertions(+), 437 deletions(-) create mode 100644 LANCommander.Launcher.Models/ConnectionState.cs create mode 100644 LANCommander.Launcher.Services/KeepAliveService.cs create mode 100644 LANCommander.Launcher/UI/Components/ConnectionStateView.razor create mode 100644 LANCommander.Launcher/UI/Components/KeepAliveContainer.razor diff --git a/LANCommander.Launcher.Models/ConnectionState.cs b/LANCommander.Launcher.Models/ConnectionState.cs new file mode 100644 index 00000000..34b0fdf0 --- /dev/null +++ b/LANCommander.Launcher.Models/ConnectionState.cs @@ -0,0 +1,7 @@ +namespace LANCommander.Launcher.Models; + +public class ConnectionState +{ + public bool IsConnected { get; set; } + public bool OfflineModeEnabled { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/AuthenticationService.cs b/LANCommander.Launcher.Services/AuthenticationService.cs index fc997b07..9cc19a79 100644 --- a/LANCommander.Launcher.Services/AuthenticationService.cs +++ b/LANCommander.Launcher.Services/AuthenticationService.cs @@ -9,11 +9,24 @@ namespace LANCommander.Launcher.Services; public class AuthenticationService : BaseService { - private readonly Settings Settings = SettingService.GetSettings(); + private readonly ProfileService ProfileService; + + private Settings Settings; + + public event EventHandler OnLogin; + public event EventHandler OnLogout; + public event EventHandler OnRegister; + + public delegate void OnOfflineModeChangedHandler(bool state); + public event OnOfflineModeChangedHandler OnOfflineModeChanged; + public AuthenticationService( Client client, - ILogger logger) : base(client, logger) + ILogger logger, + ProfileService profileService) : base(client, logger) { + Settings = SettingService.GetSettings(); + ProfileService = profileService; } public bool IsConnected() @@ -32,6 +45,109 @@ public class AuthenticationService : BaseService return false; } } + + public async Task Login() + { + await Login(Settings.Authentication.ServerAddress, new SDK.Models.AuthToken + { + AccessToken = Settings.Authentication.AccessToken, + RefreshToken = Settings.Authentication.RefreshToken, + }); + } + + public async Task Login(string serverAddress, string username, string password) + { + Client.ChangeServerAddress(serverAddress); + + var token = await Client.AuthenticateAsync(username, password); + + await Login(serverAddress, token); + } + + public async Task Login(string serverAddress, SDK.Models.AuthToken token) + { + Client.ChangeServerAddress(serverAddress); + + Settings = SettingService.GetSettings(); + + Settings.Authentication.ServerAddress = serverAddress; + Settings.Authentication.AccessToken = token.AccessToken; + Settings.Authentication.RefreshToken = token.RefreshToken; + + Client.UseToken(token); + + if (await Client.ValidateTokenAsync()) + { + SetOfflineMode(false); + + SettingService.SaveSettings(Settings); + + OnLogin?.Invoke(this, EventArgs.Empty); + + await ProfileService.DownloadProfileInfoAsync(); + } + } + + public async Task Register(string serverAddress, string username, string password, string passwordConfirmation) + { + if (String.IsNullOrWhiteSpace(serverAddress)) + throw new Exception("Server address cannot be blank"); + + if (String.IsNullOrWhiteSpace(username)) + throw new Exception("Username cannot be blank"); + + if (String.IsNullOrWhiteSpace(password)) + throw new Exception("Password cannot be blank"); + + if (password != passwordConfirmation) + throw new Exception("Passwords do not match"); + + Client.ChangeServerAddress(serverAddress); + + var token = await Client.RegisterAsync(username, password, passwordConfirmation); + + Client.UseToken(token); + + Settings = SettingService.GetSettings(); + + Settings.Authentication.ServerAddress = serverAddress; + Settings.Authentication.AccessToken = token.AccessToken; + Settings.Authentication.RefreshToken = token.RefreshToken; + + SettingService.SaveSettings(Settings); + + OnRegister?.Invoke(this, EventArgs.Empty); + + await ProfileService.DownloadProfileInfoAsync(); + } + + public void SetOfflineMode(bool state) + { + Settings = SettingService.GetSettings(); + + Settings.Authentication.OfflineMode = state; + + if (!state) + Client.Disconnect(); + + SettingService.SaveSettings(Settings); + + OnOfflineModeChanged?.Invoke(state); + } + + public async Task Logout() + { + await Client.LogoutAsync(); + + Settings = SettingService.GetSettings(); + + Settings.Profile = new ProfileSettings(); + Settings.Authentication = new AuthenticationSettings(); + + SettingService.SaveSettings(Settings); + + OnLogout?.Invoke(this, EventArgs.Empty); + } public Guid GetUserId() { @@ -75,6 +191,6 @@ public class AuthenticationService : BaseService public async Task OfflineModeAvailableAsync() { - return !IsConnected() && !(await IsServerOnlineAsync()) && HasStoredCredentials(); + return !(await IsServerOnlineAsync()) && !IsConnected() && HasStoredCredentials(); } } \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs b/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs index 08d41f13..a3c5ed9e 100644 --- a/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs @@ -21,10 +21,11 @@ namespace LANCommander.Launcher.Services.Extensions using (var scope = serviceProvider.CreateScope()) { var logger = scope.ServiceProvider.GetService(); - var profileService = scope.ServiceProvider.GetService(); + var authenticationService = scope.ServiceProvider.GetService(); + var keepAliveService = scope.ServiceProvider.GetService(); #region Sign in - profileService.Login().Wait(); + authenticationService.Login().Wait(); #endregion #region Scaffold Required Directories diff --git a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs index 0be90abb..297e2e9e 100644 --- a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs @@ -55,9 +55,10 @@ namespace LANCommander.Launcher.Services.Extensions services.AddSingleton(client); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); #endregion - - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/LANCommander.Launcher.Services/KeepAliveService.cs b/LANCommander.Launcher.Services/KeepAliveService.cs new file mode 100644 index 00000000..451acc47 --- /dev/null +++ b/LANCommander.Launcher.Services/KeepAliveService.cs @@ -0,0 +1,120 @@ +using System.Management.Automation.Language; +using LANCommander.SDK; +using System.Timers; +using Microsoft.Extensions.Logging; +using Timer = System.Timers.Timer; + +namespace LANCommander.Launcher.Services; + +public class KeepAliveService : BaseService +{ + private readonly AuthenticationService AuthenticationService; + + private Timer CheckConnectionTimer; + private Timer RetryConnectionTimer; + + private int PingInterval = 2000; + private int RetryInterval = 1000; + private int RetryCount; + private int MaxRetries = 10; + private bool ConnectionLost = false; + + public event EventHandler ConnectionSevered; + public event EventHandler ConnectionLostPermanently; + public event EventHandler ConnectionEstablished; + + public KeepAliveService( + Client client, + ILogger logger, + AuthenticationService authenticationService) : base(client, logger) + { + AuthenticationService = authenticationService; + + AuthenticationService.OnLogin += (sender, args) => StartMonitoring(); + AuthenticationService.OnLogout += (sender, args) => StopMonitoring(); + AuthenticationService.OnRegister += (sender, args) => StartMonitoring(); + AuthenticationService.OnOfflineModeChanged += (state) => + { + if (state) + StopMonitoring(); + else + StartMonitoring(); + }; + + ConnectionEstablished += (sender, args) => StartMonitoring(); + } + + public void StartMonitoring() + { + RetryCount = 0; + + CheckConnectionTimer?.Stop(); + CheckConnectionTimer?.Dispose(); + + CheckConnectionTimer = new Timer(PingInterval); + CheckConnectionTimer.Elapsed += CheckConnection; + CheckConnectionTimer.AutoReset = true; + CheckConnectionTimer.Start(); + } + + public void StopMonitoring() + { + RetryCount = 0; + CheckConnectionTimer?.Stop(); + CheckConnectionTimer?.Dispose(); + RetryConnectionTimer?.Stop(); + RetryConnectionTimer?.Dispose(); + } + + private async void CheckConnection(object sender, ElapsedEventArgs e) + { + var serverOnline = await AuthenticationService.IsServerOnlineAsync(); + + if (!serverOnline && !ConnectionLost) + { + CheckConnectionTimer.Stop(); + CheckConnectionTimer.Dispose(); + CheckConnectionTimer = null; + + ConnectionLost = true; + + ConnectionSevered?.Invoke(this, EventArgs.Empty); + + RetryConnectionTimer = new Timer(RetryInterval); + RetryConnectionTimer.Elapsed += RetryConnection; + RetryConnectionTimer.AutoReset = true; + RetryConnectionTimer.Start(); + } + } + + private async void RetryConnection(object sender, ElapsedEventArgs e) + { + var serverOnline = await AuthenticationService.IsServerOnlineAsync(); + + if (serverOnline && ConnectionLost) + { + ConnectionLost = false; + + RetryConnectionTimer.Stop(); + RetryConnectionTimer.Elapsed -= RetryConnection; + + ConnectionEstablished?.Invoke(this, EventArgs.Empty); + + AuthenticationService.SetOfflineMode(false); + } + else + { + RetryCount++; + + if (RetryCount == MaxRetries) + { + ConnectionLostPermanently?.Invoke(this, EventArgs.Empty); + + RetryConnectionTimer.Stop(); + RetryConnectionTimer.Elapsed -= RetryConnection; + + AuthenticationService.SetOfflineMode(true); + } + } + } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/ProfileService.cs b/LANCommander.Launcher.Services/ProfileService.cs index 1c27ff0f..569e1d26 100644 --- a/LANCommander.Launcher.Services/ProfileService.cs +++ b/LANCommander.Launcher.Services/ProfileService.cs @@ -26,105 +26,19 @@ namespace LANCommander.Launcher.Services Settings = SettingService.GetSettings(); } - public async Task Login() + public async Task ChangeAlias(string newName) { - await Login(Settings.Authentication.ServerAddress, new SDK.Models.AuthToken - { - AccessToken = Settings.Authentication.AccessToken, - RefreshToken = Settings.Authentication.RefreshToken, - }); - } - - public async Task Login(string serverAddress, SDK.Models.AuthToken token) - { - Client.ChangeServerAddress(serverAddress); + await Client.Profile.ChangeAliasAsync(newName); Settings = SettingService.GetSettings(); - Settings.Authentication.ServerAddress = serverAddress; - Settings.Authentication.AccessToken = token.AccessToken; - Settings.Authentication.RefreshToken = token.RefreshToken; - - Client.UseToken(token); - - if (await Client.ValidateTokenAsync()) - { - - SettingService.SaveSettings(Settings); - - var remoteProfile = await Client.Profile.GetAsync(); - - Settings.Profile.Id = remoteProfile.Id; - Settings.Profile.Alias = String.IsNullOrWhiteSpace(remoteProfile.Alias) ? remoteProfile.UserName : remoteProfile.Alias; - - try - { - var tempAvatarPath = await Client.Profile.DownloadAvatar(); - - if (!String.IsNullOrWhiteSpace(tempAvatarPath)) - { - var media = new Media - { - FileId = Guid.NewGuid(), - Type = SDK.Enums.MediaType.Avatar, - MimeType = MediaTypeNames.Image.Png, - Crc32 = SDK.Services.MediaService.CalculateChecksum(tempAvatarPath), - }; - - media = await MediaService.Add(media); - - var localPath = MediaService.GetImagePath(media); - - if (File.Exists(tempAvatarPath)) - File.Move(tempAvatarPath, localPath); - - Settings.Profile.AvatarId = media.Id; - } - } - catch (Exception ex) - { - Logger?.LogError(ex, "Could not download avatar"); - } - - SettingService.SaveSettings(Settings); - } - } - - public async Task Login(string serverAddress, string username, string password) - { - Client.ChangeServerAddress(serverAddress); - - var token = await Client.AuthenticateAsync(username, password); - - await Login(serverAddress, token); - } - - public async Task Register(string serverAddress, string username, string password, string passwordConfirmation) - { - if (String.IsNullOrWhiteSpace(serverAddress)) - throw new Exception("Server address cannot be blank"); - - if (String.IsNullOrWhiteSpace(username)) - throw new Exception("Username cannot be blank"); - - if (String.IsNullOrWhiteSpace(password)) - throw new Exception("Password cannot be blank"); - - if (password != passwordConfirmation) - throw new Exception("Passwords do not match"); - - Client.ChangeServerAddress(serverAddress); - - var token = await Client.RegisterAsync(username, password, passwordConfirmation); - - Settings = SettingService.GetSettings(); - - Settings.Authentication.ServerAddress = serverAddress; - Settings.Authentication.AccessToken = token.AccessToken; - Settings.Authentication.RefreshToken = token.RefreshToken; + Settings.Profile.Alias = newName; SettingService.SaveSettings(Settings); + } + public async Task DownloadProfileInfoAsync() + { var remoteProfile = await Client.Profile.GetAsync(); Settings.Profile.Id = remoteProfile.Id; @@ -162,38 +76,6 @@ namespace LANCommander.Launcher.Services SettingService.SaveSettings(Settings); } - public void SetOfflineMode(bool state) - { - Settings = SettingService.GetSettings(); - - Settings.Authentication.OfflineMode = false; - - SettingService.SaveSettings(Settings); - } - - public async Task Logout() - { - await Client.LogoutAsync(); - - Settings = SettingService.GetSettings(); - - Settings.Profile = new ProfileSettings(); - Settings.Authentication = new AuthenticationSettings(); - - SettingService.SaveSettings(Settings); - } - - public async Task ChangeAlias(string newName) - { - await Client.Profile.ChangeAliasAsync(newName); - - Settings = SettingService.GetSettings(); - - Settings.Profile.Alias = newName; - - SettingService.SaveSettings(Settings); - } - public bool IsAuthenticated() { return !String.IsNullOrWhiteSpace(Settings.Authentication.AccessToken); diff --git a/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor b/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor index 477a3853..57ee4911 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor +++ b/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor @@ -1,5 +1,5 @@ @using LANCommander.SDK.Models -@inject ProfileService ProfileService +@inject AuthenticationService AuthenticationService @inject SDK.Client Client @inject NavigationManager NavigationManager @inject IMessageService MessageService @@ -105,7 +105,7 @@ try { - await ProfileService.Login(Client.GetServerAddress(), Model.UserName, Model.Password); + await AuthenticationService.Login(Client.GetServerAddress(), Model.UserName, Model.Password); MainLayout.Import(); @@ -133,7 +133,7 @@ try { - await ProfileService.Login(ServerAddress, token); + await AuthenticationService.Login(ServerAddress, token); MainLayout.Import(); diff --git a/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor b/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor index 0fcd5a61..23356c9e 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor +++ b/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor @@ -1,5 +1,5 @@ @using LANCommander.SDK.Models -@inject ProfileService ProfileService +@inject AuthenticationService AuthenticationService @inject SDK.Client Client @inject NavigationManager NavigationManager @inject IMessageService MessageService @@ -52,7 +52,7 @@ try { - await ProfileService.Register(Client.GetServerAddress(), Model.UserName, Model.Password, Model.PasswordConfirmation); + await AuthenticationService.Register(Client.GetServerAddress(), Model.UserName, Model.Password, Model.PasswordConfirmation); MainLayout.Import(); @@ -78,7 +78,7 @@ try { - await ProfileService.Login(ServerAddress, token); + await AuthenticationService.Login(ServerAddress, token); MainLayout.Import(); diff --git a/LANCommander.Launcher/UI/Components/ConnectionStateView.razor b/LANCommander.Launcher/UI/Components/ConnectionStateView.razor new file mode 100644 index 00000000..2f29828b --- /dev/null +++ b/LANCommander.Launcher/UI/Components/ConnectionStateView.razor @@ -0,0 +1,15 @@ +@if (!ConnectionState.IsConnected || ConnectionState.OfflineModeEnabled) +{ + @Offline +} +else +{ + @Online +} + +@code { + [Parameter] public RenderFragment Online { get; set; } + [Parameter] public RenderFragment Offline { get; set; } + + [CascadingParameter] public Models.ConnectionState ConnectionState { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Components/Footer.razor b/LANCommander.Launcher/UI/Components/Footer.razor index 61d4de11..7990bf6b 100644 --- a/LANCommander.Launcher/UI/Components/Footer.razor +++ b/LANCommander.Launcher/UI/Components/Footer.razor @@ -12,16 +12,16 @@ } else { - if (Client.IsConnected()) - { - - } - else - { - - - - } + + + + + + + + + + } @if (InstallService.Queue.Any(qi => qi.State)) @@ -35,12 +35,16 @@ } - + + + @code { + [CascadingParameter] public bool Connected { get; set; } + bool DownloadQueueVisible = false; DownloadQueue DownloadQueue; diff --git a/LANCommander.Launcher/UI/Components/KeepAliveContainer.razor b/LANCommander.Launcher/UI/Components/KeepAliveContainer.razor new file mode 100644 index 00000000..362bd731 --- /dev/null +++ b/LANCommander.Launcher/UI/Components/KeepAliveContainer.razor @@ -0,0 +1,60 @@ +@implements IDisposable +@inject KeepAliveService KeepAliveService +@inject IMessageService MessageService + +@code { + string ConnectionMessageKey = Guid.NewGuid().ToString(); + + protected override void OnInitialized() + { + KeepAliveService.ConnectionSevered += OnConnectionSevered; + KeepAliveService.ConnectionLostPermanently += OnConnectionLostPermanently; + KeepAliveService.ConnectionEstablished += OnConnectionEstablished; + } + + void OnConnectionSevered(object? sender, EventArgs e) + { + var messageConfig = new MessageConfig + { + Key = ConnectionMessageKey, + Type = MessageType.Loading, + Content = "Lost connection, retrying...", + Duration = 0, + }; + + MessageService.Open(messageConfig); + } + + void OnConnectionLostPermanently(object? sender, EventArgs e) + { + var messageConfig = new MessageConfig + { + Key = ConnectionMessageKey, + Type = MessageType.Error, + Content = "Server unavailable, enabling offline mode", + Duration = 2.5, + }; + + MessageService.Open(messageConfig); + } + + void OnConnectionEstablished(object? sender, EventArgs e) + { + var messageConfig = new MessageConfig + { + Key = ConnectionMessageKey, + Type = MessageType.Success, + Content = "Connection established!", + Duration = 2.5, + }; + + MessageService.Open(messageConfig); + } + + public void Dispose() + { + KeepAliveService.ConnectionSevered -= OnConnectionSevered; + KeepAliveService.ConnectionLostPermanently -= OnConnectionLostPermanently; + KeepAliveService.ConnectionEstablished -= OnConnectionEstablished; + } +} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Components/MediaImage.razor b/LANCommander.Launcher/UI/Components/MediaImage.razor index 7ae251f5..755c4e52 100644 --- a/LANCommander.Launcher/UI/Components/MediaImage.razor +++ b/LANCommander.Launcher/UI/Components/MediaImage.razor @@ -12,6 +12,8 @@ [Parameter] public Guid Id { get; set; } [Parameter] public string Class { get; set; } [Parameter] public string Key { get; set; } + [CascadingParameter] public bool Connected { get; set; } + [CascadingParameter] public bool OfflineMode { get; set; } string MediaUrl { get; set; } string HiddenClass { get; set; } @@ -28,7 +30,7 @@ try { var media = await MediaService.Get(Id); - if (media == null && Client.IsConnected()) + if (media == null && (Connected || OfflineMode)) media = await ImportService.ImportMediaAsync(Id); UpdateMediaUrl(media); diff --git a/LANCommander.Launcher/UI/Components/ProfileButton.razor b/LANCommander.Launcher/UI/Components/ProfileButton.razor index ba87bd1b..1af6b377 100644 --- a/LANCommander.Launcher/UI/Components/ProfileButton.razor +++ b/LANCommander.Launcher/UI/Components/ProfileButton.razor @@ -1,18 +1,20 @@ -@inject ProfileService ProfileService +@using LANCommander.Launcher.Models +@inject ProfileService ProfileService +@inject AuthenticationService AuthenticationService @inject NavigationManager NavigationManager @inject ModalService ModalService - + Change Name Settings - @if (!Settings.Authentication.OfflineMode) + @if (!ConnectionState.OfflineModeEnabled) { Logout @@ -34,6 +36,7 @@ @code { + [CascadingParameter] public ConnectionState ConnectionState { get; set; } Models.Settings Settings = null; protected override async Task OnInitializedAsync() @@ -68,7 +71,7 @@ async Task Logout() { - await ProfileService.Logout(); + await AuthenticationService.Logout(); NavigationManager.NavigateTo("/Authenticate"); } diff --git a/LANCommander.Launcher/UI/Library/Index.razor b/LANCommander.Launcher/UI/Library/Index.razor index 98d8bb5d..2205d05b 100644 --- a/LANCommander.Launcher/UI/Library/Index.razor +++ b/LANCommander.Launcher/UI/Library/Index.razor @@ -48,7 +48,16 @@ } else if (SelectedItem.State == ListItemState.NotInstalled) { - + + + + + + + + + + } else if (SelectedItem.State == ListItemState.Queued) { @@ -56,7 +65,16 @@ } else if (SelectedItem.State == ListItemState.UpdateAvailable) { - + + + + + + + + + + } else if (SelectedItem.State == ListItemState.Installing) { @@ -64,11 +82,15 @@ } - @if (SelectedItem.State == ListItemState.NotInstalled && Client.IsConnected()) + @if (SelectedItem.State == ListItemState.NotInstalled && Connected) { - - - + + + + + + + } @@ -148,6 +170,8 @@ @code { [Parameter] public Guid Id { get; set; } + [CascadingParameter] public bool Connected { get; set; } + [CascadingParameter] public bool OfflineMode { get; set; } Models.ListItem SelectedItem { get; set; } @@ -172,7 +196,7 @@ { await OnLibraryItemSelected(await LibraryService.GetItemAsync(Id)); - if (SelectedGame != null && Client.IsConnected()) + if (SelectedGame != null && (Connected && !OfflineMode)) RemoteGame = await Client.Games.GetAsync(SelectedGame.Id); } @@ -229,6 +253,13 @@ } } + async Task Update(Models.ListItem item) + { + var game = item.DataItem as Game; + + await InstallService.Add(game); + } + async Task OnQueueChanged() { var queueItem = InstallService.Queue.FirstOrDefault(i => SelectedItem != null && i.Id == SelectedItem.Key); diff --git a/LANCommander.Launcher/UI/MainLayout.razor b/LANCommander.Launcher/UI/MainLayout.razor index a0202f7b..2e0498b2 100644 --- a/LANCommander.Launcher/UI/MainLayout.razor +++ b/LANCommander.Launcher/UI/MainLayout.razor @@ -1,85 +1,94 @@ -@using LANCommander.Launcher.Models +@using System.Management.Automation.Remoting +@using LANCommander.Launcher.Models +@using LANCommander.SDK @using Photino.Blazor.CustomWindow.Components +@using ConnectionState = LANCommander.Launcher.Models.ConnectionState @inherits LayoutComponentBase @inject ImportService ImportService @inject ProfileService ProfileService +@inject AuthenticationService AuthenticationService @inject IMessageService MessageService @inject NavigationManager NavigationManager @inject ModalService ModalService @inject LANCommander.SDK.Client LANCommander @inject IJSRuntime JS - - - - @if (Settings != null && Settings.Profile != null && ProfileService.IsAuthenticated()) - { - @if (!Settings.Authentication.OfflineMode) + + + + + + + + + + - - - - - @context.StackTrace - - - - - - - - - - + + + + + + + + + + + + + + @context.StackTrace + + + + + + + + @code { Models.Settings Settings = null; public bool Importing = false; public bool Connecting = false; + + public ConnectionState ConnectionState = new(); + public IMessageService Messages { get; set; } public static MainLayout _MainLayout { get; set; } @@ -129,12 +138,17 @@ ImportService.OnImportComplete += OnImportComplete; ImportService.OnImportUpdated += OnImportUpdated; + + AuthenticationService.OnOfflineModeChanged += OnOfflineModeChanged; var randIndex = new Random().Next(0, CrashQuips.Length - 1); RandomQuip = CrashQuips[randIndex]; - if (!(await LANCommander.ValidateTokenAsync()) && !Settings.Authentication.OfflineMode) + ConnectionState.IsConnected = await LANCommander.ValidateTokenAsync(); + ConnectionState.OfflineModeEnabled = Settings.Authentication.OfflineMode; + + if (!ConnectionState.IsConnected && !ConnectionState.OfflineModeEnabled) NavigationManager.NavigateTo("/Authenticate"); } @@ -148,6 +162,13 @@ MessageService.Info($"Importing {status.CurrentItem.Name}"); } + async void OnOfflineModeChanged(bool state) + { + _MainLayout.ConnectionState.OfflineModeEnabled = state; + _MainLayout.ConnectionState.IsConnected = LANCommander.IsConnected(); + await InvokeAsync(_MainLayout.StateHasChanged); + } + async Task CopyError(Exception ex) { await JS.InvokeVoidAsync("navigator.clipboard.writeText", ex.Message + "\n" + ex.StackTrace); @@ -169,14 +190,19 @@ if (await LANCommander.ValidateTokenAsync(token)) { - ProfileService.SetOfflineMode(false); + await AuthenticationService.Login(); + MessageService.Success("Back Online!"); + + ConnectionState.IsConnected = true; + ConnectionState.OfflineModeEnabled = false; + + await InvokeAsync(StateHasChanged); } else { if (await LANCommander.PingAsync()) { - ProfileService.SetOfflineMode(false); await Logout(); } else @@ -185,7 +211,7 @@ { Title = "Could Not Reconnect!", Icon = @, - Content = "The LANCommander server could not be reached. Click stay offline and try later, or logout and fix your credentials.", + Content = "The LANCommander server could not be reached. Click stay offline and try later, or logout and enter your credentials.", OkText = "Logout", CancelText = "Stay Offline", Centered = true, @@ -203,7 +229,10 @@ async Task Logout() { - await ProfileService.Logout(); + await AuthenticationService.Logout(); + + ConnectionState.IsConnected = false; + ConnectionState.OfflineModeEnabled = false; NavigationManager.NavigateTo("/Authenticate"); } diff --git a/LANCommander.SDK/Client.cs b/LANCommander.SDK/Client.cs index 62c6111d..9c53622b 100644 --- a/LANCommander.SDK/Client.cs +++ b/LANCommander.SDK/Client.cs @@ -57,6 +57,8 @@ namespace LANCommander.SDK } } + public EventHandler OnError; + public Client(string baseUrl, string defaultInstallDirectory) { DefaultInstallDirectory = defaultInstallDirectory; @@ -129,195 +131,286 @@ namespace LANCommander.SDK internal T PostRequest(string route, object body, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddJsonBody(body) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = ApiClient.Post(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddJsonBody(body) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Post(request); - - return response; + } } internal T PostRequest(string route, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = ApiClient.Post(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Post(request); - - return response; + } } internal async Task PostRequestAsync(string route, object body, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddJsonBody(body) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = await ApiClient.PostAsync(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddJsonBody(body) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PostAsync(request); - - return response; + } } internal async Task PostRequestAsync(string route, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = await ApiClient.PostAsync(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PostAsync(request); - - return response; + } } internal async Task PutRequestAsync(string route, object body, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = await ApiClient.PutAsync(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PutAsync(request); - - return response; + } } internal T GetRequest(string route, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = ApiClient.Get(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Get(request); - - return response; + } } internal async Task GetRequestAsync(string route, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = await ApiClient.GetAsync(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.GetAsync(request); - - return response; + } } internal async Task DeleteRequestAsync(string route, bool ignoreVersion = false) { - if (Token == null) + try + { + if (Token == null) + return default; + + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; + + var response = await ApiClient.DeleteAsync(request); + + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.DeleteAsync(request); - - return response; + } } internal async Task DownloadRequestAsync(string route, Action progressHandler, Action completeHandler) { - route = route.TrimStart('/'); - - var client = new WebClient(); - var tempFile = Path.GetTempFileName(); - - client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); - client.DownloadProgressChanged += (s, e) => progressHandler(e); - client.DownloadFileCompleted += (s, e) => completeHandler(e); - try { - await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), tempFile); + route = route.TrimStart('/'); + + var client = new WebClient(); + var tempFile = Path.GetTempFileName(); + + client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); + client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); + client.DownloadProgressChanged += (s, e) => progressHandler(e); + client.DownloadFileCompleted += (s, e) => completeHandler(e); + + try + { + await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), tempFile); + } + catch (Exception ex) + { + Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", route); + + if (File.Exists(tempFile)) + File.Delete(tempFile); + + tempFile = String.Empty; + } + + return tempFile; } catch (Exception ex) { - Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", route); - - if (File.Exists(tempFile)) - File.Delete(tempFile); - - tempFile = String.Empty; + OnError?.Invoke(this, ex); + + return null; } - - return tempFile; } internal async Task DownloadRequestAsync(string route, string destination) { - route = route.TrimStart('/'); - - var client = new WebClient(); - - client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); - try { - await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), destination); + route = route.TrimStart('/'); + + var client = new WebClient(); + + client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); + client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); + + try + { + await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), destination); + } + catch (Exception ex) + { + Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", + route); + + if (File.Exists(destination)) + File.Delete(destination); + + destination = String.Empty; + } + + return destination; } catch (Exception ex) { - Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", route); - - if (File.Exists(destination)) - File.Delete(destination); - - destination = String.Empty; + OnError?.Invoke(this, ex); + + return null; } - - return destination; } internal TrackableStream StreamRequest(string route) @@ -336,123 +429,164 @@ namespace LANCommander.SDK internal T UploadRequest(string route, string fileName, byte[] data, bool ignoreVersion = false) { - var request = new RestRequest(route, Method.Post) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + try + { + var request = new RestRequest(route, Method.Post) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; - request.AddFile(fileName, data, fileName); + request.AddFile(fileName, data, fileName); - var response = ApiClient.Post(request); + var response = ApiClient.Post(request); - return response; + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + + return default; + } } internal async Task UploadRequestAsync(string route, string fileName, byte[] data, bool ignoreVersion = false) { - var request = new RestRequest(route, Method.Post) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); + try + { + var request = new RestRequest(route, Method.Post) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}") + .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; - request.AddFile(fileName, data, fileName); + request.AddFile(fileName, data, fileName); - var response = await ApiClient.PostAsync(request); + var response = await ApiClient.PostAsync(request); - return response; + return response; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + + return default; + } } internal async Task ChunkedUploadRequestAsync(string fileName, Stream stream, bool ignoreVersion = false) { - var maxChunkSize = 1024 * 1024 * 50; - var initResponse = await PostRequestAsync("/Upload/Init", ignoreVersion); - - var buffer = new byte[maxChunkSize]; - - while (stream.Position < stream.Length) + try { - var chunkRequest = new UploadChunkRequest(); + var maxChunkSize = 1024 * 1024 * 50; + var initResponse = await PostRequestAsync("/Upload/Init", ignoreVersion); - chunkRequest.Start = stream.Position; + var buffer = new byte[maxChunkSize]; - if (stream.Position + maxChunkSize > stream.Length) + while (stream.Position < stream.Length) { - var bytes = stream.Length - stream.Position; + var chunkRequest = new UploadChunkRequest(); - buffer = new byte[bytes]; + chunkRequest.Start = stream.Position; - await stream.ReadAsync(buffer, 0, (int)(stream.Length - stream.Position)); + if (stream.Position + maxChunkSize > stream.Length) + { + var bytes = stream.Length - stream.Position; + + buffer = new byte[bytes]; + + await stream.ReadAsync(buffer, 0, (int)(stream.Length - stream.Position)); + } + else + await stream.ReadAsync(buffer, 0, maxChunkSize); + + chunkRequest.End = stream.Position; + chunkRequest.Total = stream.Length; + chunkRequest.File = buffer; + chunkRequest.Key = initResponse.Key; + + await PostRequestAsync("/Upload/Chunk", chunkRequest, ignoreVersion); } - else - await stream.ReadAsync(buffer, 0, maxChunkSize); - chunkRequest.End = stream.Position; - chunkRequest.Total = stream.Length; - chunkRequest.File = buffer; - chunkRequest.Key = initResponse.Key; - - await PostRequestAsync("/Upload/Chunk", chunkRequest, ignoreVersion); + return initResponse.Key; + } + catch (Exception ex) + { + OnError?.Invoke(this, ex); + + return default; } - - return initResponse.Key; } public async Task AuthenticateAsync(string username, string password, bool ignoreVersion = false) { - var request = new RestRequest("/api/Auth/Login", Method.Post); - - request.AddJsonBody(new AuthRequest() + try { - UserName = username, - Password = password - }); + var request = new RestRequest("/api/Auth/Login", Method.Post); - if (!ignoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; + request.AddJsonBody(new AuthRequest() + { + UserName = username, + Password = password + }); - var response = await ApiClient.ExecuteAsync(request); + if (!ignoreVersion) + request.Interceptors = new List() { new VersionInterceptor() }; - if (response.ErrorException != null) - { - Logger?.LogError(response.ErrorException, "Authentication failed for user {UserName}", username); + var response = await ApiClient.ExecuteAsync(request); - throw response.ErrorException; + if (response.ErrorException != null) + { + Logger?.LogError(response.ErrorException, "Authentication failed for user {UserName}", username); + + throw response.ErrorException; + } + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + var token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + UseToken(token); + + Connected = true; + + return token; + + case HttpStatusCode.Forbidden: + case HttpStatusCode.BadRequest: + case HttpStatusCode.Unauthorized: + Connected = false; + Logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username); + throw new WebException("Invalid username or password"); + + default: + Connected = false; + Logger?.LogError("Authentication failed for user {UserName}: could not communicate with the server", username); + throw new WebException("Could not communicate with the server"); + } } - - switch (response.StatusCode) + catch (Exception ex) { - case HttpStatusCode.OK: - var token = new AuthToken - { - AccessToken = response.Data.AccessToken, - RefreshToken = response.Data.RefreshToken, - Expiration = response.Data.Expiration - }; - - UseToken(token); - - Connected = true; - - return token; - - case HttpStatusCode.Forbidden: - case HttpStatusCode.BadRequest: - case HttpStatusCode.Unauthorized: - Connected = false; - Logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username); - throw new WebException("Invalid username or password"); - - default: - Connected = false; - Logger?.LogError("Authentication failed for user {UserName}: could not communicate with the server", username); - throw new WebException("Could not communicate with the server"); + OnError?.Invoke(this, ex); + + return default; } } + public void Disconnect() + { + Connected = false; + } + public async Task LogoutAsync() { await ApiClient.ExecuteAsync(new RestRequest("/api/Auth/Logout", Method.Post)); @@ -476,6 +610,8 @@ namespace LANCommander.SDK if (!String.IsNullOrWhiteSpace(response?.Data?.Message)) { Logger?.LogError(response.Data.Message); + + OnError?.Invoke(this, response.ErrorException); throw new Exception(response.Data.Message); } @@ -521,7 +657,7 @@ namespace LANCommander.SDK public async Task PingAsync() { - var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.Get)); + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.Head)); return response.StatusCode == HttpStatusCode.OK; }