diff --git a/Moonlight/Core/Configuration/CoreConfiguration.cs b/Moonlight/Core/Configuration/CoreConfiguration.cs index db0e59fa..f89e839a 100644 --- a/Moonlight/Core/Configuration/CoreConfiguration.cs +++ b/Moonlight/Core/Configuration/CoreConfiguration.cs @@ -90,6 +90,16 @@ public class CoreConfiguration [JsonProperty("DenyRegister")] [Description("This disables the register function. No user will be able to sign up anymore. Its recommended to enable this for private instances")] public bool DenyRegister { get; set; } = false; + + [JsonProperty("EnablePeriodicReAuth")] + [Description( + "If this option is enabled, every session will reauthenticate perdiodicly to track state changes in real time without the user refreshing the page")] + public bool EnablePeriodicReAuth { get; set; } = true; + + [JsonProperty("PeriodicReAuthDelay")] + [Description( + "This option specifies how long the intervals are between reauthentications. The value is specified in minutes")] + public int PeriodicReAuthDelay { get; set; } = 5; } public class SecurityData diff --git a/Moonlight/Core/CoreFeature.cs b/Moonlight/Core/CoreFeature.cs index e7ad9e81..2be4032a 100644 --- a/Moonlight/Core/CoreFeature.cs +++ b/Moonlight/Core/CoreFeature.cs @@ -3,14 +3,10 @@ using Microsoft.AspNetCore.Components; using MoonCore.Abstractions; using MoonCore.Helpers; using MoonCore.Services; -using MoonCoreUI.Extensions; -using MoonCoreUI.Services; using Moonlight.Core.Configuration; using Moonlight.Core.Database; using Moonlight.Core.Database.Entities; using Moonlight.Core.Implementations.Diagnose; -using Moonlight.Core.Implementations.UI.Admin.AdminColumns; -using Moonlight.Core.Implementations.UI.Index; using Moonlight.Core.Interfaces; using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Interfaces.UI.User; @@ -21,10 +17,16 @@ using Moonlight.Core.Models.Enums; using Moonlight.Core.Repositories; using Moonlight.Core.Services; using Microsoft.OpenApi.Models; +using MoonCore.Blazor.Extensions; +using MoonCore.Blazor.Services; +using MoonCore.Extensions; using Moonlight.Core.Attributes; using Moonlight.Core.Http.Middleware; +using Moonlight.Core.Implementations.AdminDashboard; using Moonlight.Core.Implementations.ApiDefinition; +using Moonlight.Core.Implementations.UserDashboard; using Swashbuckle.AspNetCore.SwaggerGen; +using AuthenticationStateProvider = Moonlight.Core.Helpers.AuthenticationStateProvider; using Microsoft.AspNetCore.Http.Features; namespace Moonlight.Core; @@ -55,25 +57,24 @@ public class CoreFeature : MoonlightFeature builder.Services.AddDbContext(); // - builder.Services.AddSingleton(new JwtService(config.Security.Token)); + builder.Services.AddSingleton(new JwtService( + config.Security.Token, + context.LoggerFactory.CreateLogger>() + ) + ); // Mooncore services builder.Services.AddScoped(typeof(Repository<>), typeof(GenericRepository<>)); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - builder.Services.AddMoonCoreUi(configuration => + + builder.Services.AddMoonCore(configuration => { - configuration.ToastJavascriptPrefix = "moonlight.toasts"; - configuration.ModalJavascriptPrefix = "moonlight.modals"; - configuration.AlertJavascriptPrefix = "moonlight.alerts"; - configuration.ClipboardJavascriptPrefix = "moonlight.clipboard"; - configuration.FileDownloadJavascriptPrefix = "moonlight.utils"; + configuration.Identity.Token = config.Security.Token; + configuration.Identity.PeriodicReAuthDelay = TimeSpan.FromMinutes(config.Authentication.PeriodicReAuthDelay); + configuration.Identity.EnablePeriodicReAuth = config.Authentication.EnablePeriodicReAuth; + configuration.Identity.Provider = new AuthenticationStateProvider(); }); + + builder.Services.AddMoonCoreBlazor(); // Add external services and blazor/asp.net stuff builder.Services.AddRazorPages(); @@ -191,7 +192,7 @@ public class CoreFeature : MoonlightFeature Name = "Manage admin api access", Description = "Allows access to manage api keys and their permissions" }); - + await permissionService.Register(9999, new() { Name = "Manage system", @@ -226,9 +227,9 @@ public class CoreFeature : MoonlightFeature { using var scope = provider.CreateScope(); - var configService = scope.ServiceProvider.GetRequiredService>(); var userRepo = scope.ServiceProvider.GetRequiredService>(); var authenticationProvider = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); if (!configService.Get().Authentication.UseDefaultAuthentication) return; @@ -246,7 +247,7 @@ public class CoreFeature : MoonlightFeature if (registeredUser == null) { - Logger.Warn("Unable to create default user. Register function returned null"); + logger.LogWarning("Unable to create default user. Register function returned null"); return; } @@ -255,7 +256,7 @@ public class CoreFeature : MoonlightFeature user.Permissions = 9999; userRepo.Update(user); - Logger.Info($"Default login: Email: '{email}' Password: '{password}'"); + logger.LogInformation("Default login: Email: '{email}' Password: '{password}'", email, password); }); // Api @@ -263,7 +264,7 @@ public class CoreFeature : MoonlightFeature app.MapSwagger("/api/core/reference/openapi/{documentName}"); app.UseMiddleware(); - + await pluginService.RegisterImplementation(new InternalApiDefinition()); } diff --git a/Moonlight/Core/Database/DataContext.cs b/Moonlight/Core/Database/DataContext.cs index d987ada6..8ebd658d 100644 --- a/Moonlight/Core/Database/DataContext.cs +++ b/Moonlight/Core/Database/DataContext.cs @@ -50,4 +50,9 @@ public class DataContext : DbContext ); } } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + } } \ No newline at end of file diff --git a/Moonlight/Core/Events/CoreEvents.cs b/Moonlight/Core/Events/CoreEvents.cs index 5a02cfa6..cd3ece06 100644 --- a/Moonlight/Core/Events/CoreEvents.cs +++ b/Moonlight/Core/Events/CoreEvents.cs @@ -1,8 +1,15 @@ -using MoonCore.Helpers; +using MoonCore.Attributes; +using MoonCore.Helpers; namespace Moonlight.Core.Events; +[Singleton] public class CoreEvents { - public static SmartEventHandler OnMoonlightRestart { get; set; } = new(); + public CoreEvents(ILogger logger) + { + OnMoonlightRestart = new(logger); + } + + public SmartEventHandler OnMoonlightRestart { get; set; } } \ No newline at end of file diff --git a/Moonlight/Core/Extensions/IdentityServiceExtensions.cs b/Moonlight/Core/Extensions/IdentityServiceExtensions.cs new file mode 100644 index 00000000..83142cae --- /dev/null +++ b/Moonlight/Core/Extensions/IdentityServiceExtensions.cs @@ -0,0 +1,54 @@ +using MoonCore.Abstractions; +using MoonCore.Services; +using Moonlight.Core.Database.Entities; + +namespace Moonlight.Core.Extensions; + +public static class IdentityServiceExtensions +{ + public static User GetUser(this IdentityService identityService) + { + return identityService.Storage.Get(); + } + + public static Task HasFlag(this IdentityService identityService, string flag) + { + if (!identityService.IsAuthenticated) + return Task.FromResult(false); + + var result = identityService.GetUser().Flags.Split(";").Contains(flag); + return Task.FromResult(result); + } + + public static Task SetFlag(this IdentityService identityService, string flag, bool toggle) + { + if (!identityService.IsAuthenticated) + return Task.CompletedTask; + + var user = identityService.GetUser(); + + // Rebuild flags + var flags = user.Flags.Split(";").ToList(); + + if (toggle) + { + if(!flags.Contains(flag)) + flags.Add(flag); + } + else + { + if (flags.Contains(flag)) + flags.Remove(flag); + } + + user.Flags = string.Join(';', flags); + + // Save changes + var serviceProvider = identityService.Storage.Get(); + var userRepo = serviceProvider.GetRequiredService>(); + + userRepo.Update(user); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Core/Helpers/AuthenticationStateProvider.cs b/Moonlight/Core/Helpers/AuthenticationStateProvider.cs new file mode 100644 index 00000000..4b830999 --- /dev/null +++ b/Moonlight/Core/Helpers/AuthenticationStateProvider.cs @@ -0,0 +1,50 @@ +using MoonCore.Abstractions; +using MoonCore.Helpers; +using Moonlight.Core.Database.Entities; + +namespace Moonlight.Core.Helpers; + +public class AuthenticationStateProvider : MoonCore.Abstractions.AuthenticationStateProvider +{ + public override Task IsValidIdentifier(IServiceProvider provider, string identifier) + { + if(!int.TryParse(identifier, out int searchId)) + return Task.FromResult(false); + + var userRepo = provider.GetRequiredService>(); + var result = userRepo.Get().Any(x => x.Id == searchId); + + return Task.FromResult(result); + } + + public override Task LoadFromIdentifier(IServiceProvider provider, string identifier, DynamicStorage storage) + { + if(!int.TryParse(identifier, out int searchId)) + return Task.CompletedTask; + + var userRepo = provider.GetRequiredService>(); + var user = userRepo.Get().FirstOrDefault(x => x.Id == searchId); + + if(user == null) + return Task.CompletedTask; + + storage.Set("User", user); + storage.Set("ServiceProvider", provider); + + return Task.CompletedTask; + } + + public override Task DetermineTokenValidTimestamp(IServiceProvider provider, string identifier) + { + if(!int.TryParse(identifier, out int searchId)) + return Task.FromResult(DateTime.MaxValue); + + var userRepo = provider.GetRequiredService>(); + var user = userRepo.Get().FirstOrDefault(x => x.Id == searchId); + + if(user == null) + return Task.FromResult(DateTime.MaxValue); + + return Task.FromResult(user.TokenValidTimestamp); + } +} \ No newline at end of file diff --git a/Moonlight/Core/Helpers/HostSystemHelper.cs b/Moonlight/Core/Helpers/HostSystemHelper.cs index 0e33ffc9..8b8ec092 100644 --- a/Moonlight/Core/Helpers/HostSystemHelper.cs +++ b/Moonlight/Core/Helpers/HostSystemHelper.cs @@ -1,12 +1,21 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using MoonCore.Attributes; using MoonCore.Helpers; namespace Moonlight.Core.Helpers; -public static class HostSystemHelper +[Singleton] +public class HostSystemHelper { - public static Task GetOsName() + private readonly ILogger Logger; + + public HostSystemHelper(ILogger logger) + { + Logger = logger; + } + + public Task GetOsName() { try { @@ -48,21 +57,20 @@ public static class HostSystemHelper } catch (Exception e) { - Logger.Warn("Error retrieving os information"); - Logger.Warn(e); + Logger.LogWarning("Error retrieving os information: {e}", e); return Task.FromResult("N/A"); } } - public static Task GetMemoryUsage() + public Task GetMemoryUsage() { var process = Process.GetCurrentProcess(); var bytes = process.PrivateMemorySize64; return Task.FromResult(bytes); } - public static Task GetCpuUsage() + public Task GetCpuUsage() { var process = Process.GetCurrentProcess(); var cpuTime = process.TotalProcessorTime; diff --git a/Moonlight/Core/Http/Controllers/AssetController.cs b/Moonlight/Core/Http/Controllers/AssetController.cs index 573f0b29..4f5b7384 100644 --- a/Moonlight/Core/Http/Controllers/AssetController.cs +++ b/Moonlight/Core/Http/Controllers/AssetController.cs @@ -9,13 +9,20 @@ namespace Moonlight.Core.Http.Controllers; [Route("api/core/asset")] public class AssetController : Controller { + private readonly ILogger Logger; + + public AssetController(ILogger logger) + { + Logger = logger; + } + [HttpGet("{name}/{*path}")] public async Task Get(string name, string path) { // Check for path transversal attacks if (path.Contains("..") || name.Contains("..")) { - Logger.Warn($"{HttpContext.Connection.RemoteIpAddress} tried to use path transversal attack: {name}/{path}"); + Logger.LogWarning("{remoteIp} tried to use path transversal attack: {name}/{path}", HttpContext.Connection.RemoteIpAddress, name, path); return NotFound(); } diff --git a/Moonlight/Core/Http/Controllers/AvatarController.cs b/Moonlight/Core/Http/Controllers/AvatarController.cs index 5a77a6d4..696463cf 100644 --- a/Moonlight/Core/Http/Controllers/AvatarController.cs +++ b/Moonlight/Core/Http/Controllers/AvatarController.cs @@ -2,12 +2,11 @@ using System.Text; using Microsoft.AspNetCore.Mvc; using MoonCore.Abstractions; -using MoonCore.Helpers; using MoonCore.Services; using Moonlight.Core.Attributes; using Moonlight.Core.Configuration; using Moonlight.Core.Database.Entities; -using Moonlight.Core.Services; +using Moonlight.Core.Extensions; namespace Moonlight.Core.Http.Controllers; @@ -19,15 +18,17 @@ public class AvatarController : Controller private readonly Repository UserRepository; private readonly ConfigService ConfigService; private readonly IdentityService IdentityService; + private readonly ILogger Logger; public AvatarController( Repository userRepository, IdentityService identityService, - ConfigService configService) + ConfigService configService, ILogger logger) { UserRepository = userRepository; IdentityService = identityService; ConfigService = configService; + Logger = logger; } [HttpGet] @@ -39,10 +40,10 @@ public class AvatarController : Controller var token = Request.Cookies["token"]; await IdentityService.Authenticate(token!); - if (!IdentityService.IsLoggedIn) + if (!IdentityService.IsAuthenticated) return StatusCode(403); - return File(await GetAvatar(IdentityService.CurrentUser), "image/jpeg"); + return File(await GetAvatar(IdentityService.GetUser()), "image/jpeg"); } [HttpGet("{id:int}")] @@ -54,12 +55,12 @@ public class AvatarController : Controller var token = Request.Cookies["token"]; await IdentityService.Authenticate(token!); - if (!IdentityService.IsLoggedIn) + if (!IdentityService.IsAuthenticated) return StatusCode(403); if (ConfigService.Get().Security.EnforceAvatarPrivacy && // Do we need to enforce privacy? - id != IdentityService.CurrentUser.Id && // is the user not viewing his own image? - IdentityService.CurrentUser.Permissions < 1000) // and not an admin? + id != IdentityService.GetUser().Id && // is the user not viewing his own image? + IdentityService.GetUser().Permissions < 1000) // and not an admin? { return StatusCode(403); } @@ -88,11 +89,10 @@ public class AvatarController : Controller catch (Exception e) { if(e is HttpRequestException requestException && requestException.InnerException is IOException ioException) - Logger.Warn($"Unable to fetch gravatar for user {user.Id}. Is moonlight inside a proxy requiring network?: {ioException.Message}"); + Logger.LogWarning("Unable to fetch gravatar for user {userId}. Is moonlight inside a proxy requiring network?: {message}", user.Id, ioException.Message); else { - Logger.Warn($"Unable to fetch gravatar for user {user.Id}"); - Logger.Warn(e); + Logger.LogWarning("Unable to fetch gravatar for user {userId}: {e}", user.Id, e); } return new MemoryStream(); diff --git a/Moonlight/Core/Http/Middleware/DebugLogMiddleware.cs b/Moonlight/Core/Http/Middleware/DebugLogMiddleware.cs index 3a679271..c55b78b1 100644 --- a/Moonlight/Core/Http/Middleware/DebugLogMiddleware.cs +++ b/Moonlight/Core/Http/Middleware/DebugLogMiddleware.cs @@ -4,16 +4,18 @@ namespace Moonlight.Core.Http.Middleware; public class DebugLogMiddleware { + private readonly ILogger Logger; private RequestDelegate Next; - public DebugLogMiddleware(RequestDelegate next) + public DebugLogMiddleware(RequestDelegate next, ILogger logger) { Next = next; + Logger = logger; } public async Task Invoke(HttpContext context) { - Logger.Debug($"[{context.Request.Method.ToUpper()}] {context.Request.Path}"); + Logger.LogDebug("[{method}] {path}", context.Request.Method.ToUpper(), context.Request.Path); await Next(context); } diff --git a/Moonlight/Core/Implementations/UI/Admin/AdminColumns/UserCount.cs b/Moonlight/Core/Implementations/AdminDashboard/UserCount.cs similarity index 81% rename from Moonlight/Core/Implementations/UI/Admin/AdminColumns/UserCount.cs rename to Moonlight/Core/Implementations/AdminDashboard/UserCount.cs index 328cb078..456fca73 100644 --- a/Moonlight/Core/Implementations/UI/Admin/AdminColumns/UserCount.cs +++ b/Moonlight/Core/Implementations/AdminDashboard/UserCount.cs @@ -1,9 +1,9 @@ -using MoonCoreUI.Helpers; +using MoonCore.Blazor.Helpers; using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Models.Abstractions; using Moonlight.Core.UI.Components.Cards; -namespace Moonlight.Core.Implementations.UI.Admin.AdminColumns; +namespace Moonlight.Core.Implementations.AdminDashboard; public class UserCount : IAdminDashboardColumn { diff --git a/Moonlight/Core/Implementations/UI/Index/GreetingMessages.cs b/Moonlight/Core/Implementations/UserDashboard/GreetingMessages.cs similarity index 83% rename from Moonlight/Core/Implementations/UI/Index/GreetingMessages.cs rename to Moonlight/Core/Implementations/UserDashboard/GreetingMessages.cs index 552ec27f..b78d9c77 100644 --- a/Moonlight/Core/Implementations/UI/Index/GreetingMessages.cs +++ b/Moonlight/Core/Implementations/UserDashboard/GreetingMessages.cs @@ -1,9 +1,9 @@ -using MoonCoreUI.Helpers; +using MoonCore.Blazor.Helpers; using Moonlight.Core.Interfaces.UI.User; using Moonlight.Core.Models.Abstractions; using Moonlight.Core.UI.Components.Cards; -namespace Moonlight.Core.Implementations.UI.Index; +namespace Moonlight.Core.Implementations.UserDashboard; public class GreetingMessages : IUserDashboardComponent { diff --git a/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs b/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs index dee6415f..957a1f18 100644 --- a/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs +++ b/Moonlight/Core/Models/Abstractions/Feature/PreInitContext.cs @@ -9,6 +9,7 @@ public class PreInitContext public List DiAssemblies { get; set; } = new(); public Dictionary> Assets { get; set; } = new(); public PluginService Plugins { get; set; } + public ILoggerFactory LoggerFactory { get; set; } public void EnableDependencyInjection() { diff --git a/Moonlight/Core/Models/Abstractions/Feature/SessionInitContext.cs b/Moonlight/Core/Models/Abstractions/Feature/SessionInitContext.cs index 75a94c58..85bc013e 100644 --- a/Moonlight/Core/Models/Abstractions/Feature/SessionInitContext.cs +++ b/Moonlight/Core/Models/Abstractions/Feature/SessionInitContext.cs @@ -1,4 +1,4 @@ -using MoonCoreUI.Components; +using MoonCore.Blazor.Components; namespace Moonlight.Core.Models.Abstractions.Feature; diff --git a/Moonlight/Core/Models/Forms/ChangeCookiesForm.cs b/Moonlight/Core/Models/Forms/ChangeCookiesForm.cs deleted file mode 100644 index 0090240e..00000000 --- a/Moonlight/Core/Models/Forms/ChangeCookiesForm.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel; - -namespace Moonlight.Core.Models.Forms; - -public class ChangeCookiesForm -{ - [Description("This specifies if you would like to personalize your experience with optional cookies.")] - public bool UseOptionalCookies { get; set; } = false; -} \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/ChangePasswordForm.cs b/Moonlight/Core/Models/Forms/ChangePasswordForm.cs index cd26be4e..9db305b0 100644 --- a/Moonlight/Core/Models/Forms/ChangePasswordForm.cs +++ b/Moonlight/Core/Models/Forms/ChangePasswordForm.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using MoonCoreUI.Attributes; namespace Moonlight.Core.Models.Forms; @@ -8,12 +7,12 @@ public class ChangePasswordForm [Required(ErrorMessage = "You need to provide a password")] [MinLength(8, ErrorMessage = "The password must be at least 8 characters long")] [MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")] - [CustomFormType(Type = "password")] + //TODO: [CustomFormType(Type = "password")] public string Password { get; set; } [Required(ErrorMessage = "You need to provide a password")] [MinLength(8, ErrorMessage = "The password must be at least 8 characters long")] [MaxLength(256, ErrorMessage = "The password must not be longer than 256 characters")] - [CustomFormType(Type = "password")] + //TODO: [CustomFormType(Type = "password")] public string RepeatedPassword { get; set; } } \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/UpdateAccountForm.cs b/Moonlight/Core/Models/Forms/UpdateAccountForm.cs index f5fdafc3..f3b6b54c 100644 --- a/Moonlight/Core/Models/Forms/UpdateAccountForm.cs +++ b/Moonlight/Core/Models/Forms/UpdateAccountForm.cs @@ -4,12 +4,6 @@ namespace Moonlight.Core.Models.Forms; public class UpdateAccountForm { - [Required(ErrorMessage = "You need to provide an username")] - [MinLength(6, ErrorMessage = "The username is too short")] - [MaxLength(20, ErrorMessage = "The username cannot be longer than 20 characters")] public string Username { get; set; } - - [Required(ErrorMessage = "You need to provide an email address")] - [EmailAddress(ErrorMessage = "You need to enter a valid email address")] public string Email { get; set; } = ""; } \ No newline at end of file diff --git a/Moonlight/Core/Models/Forms/Users/UpdateUserForm.cs b/Moonlight/Core/Models/Forms/Users/UpdateUserForm.cs index 2db50fec..342d1fa2 100644 --- a/Moonlight/Core/Models/Forms/Users/UpdateUserForm.cs +++ b/Moonlight/Core/Models/Forms/Users/UpdateUserForm.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using MoonCoreUI.Attributes; namespace Moonlight.Core.Models.Forms.Users; @@ -17,7 +16,7 @@ public class UpdateUserForm public string Email { get; set; } [Description("This toggles the use of the two factor authentication")] - [RadioButtonBool("Enabled", "Disabled", TrueIcon = "bx-lock-alt", FalseIcon = "bx-lock-open-alt")] + //TODO: [RadioButtonBool("Enabled", "Disabled", TrueIcon = "bx-lock-alt", FalseIcon = "bx-lock-open-alt")] [DisplayName("Two factor authentication")] public bool Totp { get; set; } = false; } \ No newline at end of file diff --git a/Moonlight/Core/Models/Session.cs b/Moonlight/Core/Models/Session.cs index e36759a5..0474e7f7 100644 --- a/Moonlight/Core/Models/Session.cs +++ b/Moonlight/Core/Models/Session.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -using MoonCoreUI.Services; -using Moonlight.Core.Services; +using MoonCore.Blazor.Services; +using MoonCore.Services; namespace Moonlight.Core.Models; diff --git a/Moonlight/Core/Services/FeatureService.cs b/Moonlight/Core/Services/FeatureService.cs index 5e9f3445..0e970f01 100644 --- a/Moonlight/Core/Services/FeatureService.cs +++ b/Moonlight/Core/Services/FeatureService.cs @@ -1,8 +1,7 @@ using System.Reflection; +using MoonCore.Blazor.Components; using MoonCore.Extensions; -using MoonCore.Helpers; using MoonCore.Services; -using MoonCoreUI.Components; using Moonlight.Core.Configuration; using Moonlight.Core.Models.Abstractions.Feature; @@ -15,15 +14,18 @@ public class FeatureService private readonly List Features = new(); private readonly ConfigService ConfigService; + + private readonly ILogger Logger; - public FeatureService(ConfigService configService) + public FeatureService(ConfigService configService, ILogger logger) { ConfigService = configService; + Logger = logger; } public Task Load() { - Logger.Info("Loading features"); + Logger.LogInformation("Loading features"); // TODO: Add dll loading here as well @@ -43,24 +45,25 @@ public class FeatureService if (feature == null) { - Logger.Warn($"Unable to construct {featureType.FullName} feature"); + Logger.LogWarning("Unable to construct '{name}' feature", featureType.FullName); continue; } Features.Add(feature); - Logger.Info($"Loaded feature '{feature.Name}' by '{feature.Author}'"); + Logger.LogInformation("Loaded feature '{name}' by '{author}'", feature.Name, feature.Author); } return Task.CompletedTask; } - public async Task PreInit(WebApplicationBuilder builder, PluginService pluginService) + public async Task PreInit(WebApplicationBuilder builder, PluginService pluginService, ILoggerFactory preRunLoggerFactory) { - Logger.Info("Pre-initializing features"); + Logger.LogInformation("Pre-initializing features"); PreInitContext.Builder = builder; PreInitContext.Plugins = pluginService; + PreInitContext.LoggerFactory = preRunLoggerFactory; foreach (var feature in Features) { @@ -70,8 +73,7 @@ public class FeatureService } catch (Exception e) { - Logger.Error($"An error occured while performing pre init for feature '{feature.Name}'"); - Logger.Error(e); + Logger.LogError("An error occured while performing pre init for feature '{name}': {e}", feature.Name, e); } } @@ -82,7 +84,7 @@ public class FeatureService public async Task Init(WebApplication application) { - Logger.Info("Initializing features"); + Logger.LogInformation("Initializing features"); var initContext = new InitContext() { @@ -97,15 +99,14 @@ public class FeatureService } catch (Exception e) { - Logger.Error($"An error occured while performing init for feature '{feature.Name}'"); - Logger.Error(e); + Logger.LogError("An error occured while performing init for feature '{name}': {e}", feature.Name, e); } } } public async Task UiInit() { - Logger.Info("Initializing feature uis"); + Logger.LogInformation("Initializing feature uis"); foreach (var feature in Features) { @@ -115,8 +116,7 @@ public class FeatureService } catch (Exception e) { - Logger.Error($"An error occured while performing ui init for feature '{feature.Name}'"); - Logger.Error(e); + Logger.LogError("An error occured while performing ui init for feature '{name}': {e}", feature.Name, e); } } } @@ -138,8 +138,7 @@ public class FeatureService } catch (Exception e) { - Logger.Error($"An error occured while performing session init for feature '{feature.Name}'"); - Logger.Error(e); + Logger.LogError("An error occured while performing session init for feature '{name}': {e}", feature.Name, e); } } } @@ -159,8 +158,7 @@ public class FeatureService } catch (Exception e) { - Logger.Error($"An error occured while performing session dispose for feature '{feature.Name}'"); - Logger.Error(e); + Logger.LogError("An error occured while performing session dispose for feature '{name}': {e}", feature.Name, e); } } } diff --git a/Moonlight/Core/Services/HotKeyService.cs b/Moonlight/Core/Services/HotKeyService.cs index 2655fb93..25c8c527 100644 --- a/Moonlight/Core/Services/HotKeyService.cs +++ b/Moonlight/Core/Services/HotKeyService.cs @@ -11,11 +11,13 @@ public class HotKeyService private readonly IJSRuntime JsRuntime; private readonly List HotKeys = new(); - public SmartEventHandler HotKeyPressed { get; set; } = new(); + public SmartEventHandler HotKeyPressed { get; set; } - public HotKeyService(IJSRuntime jsRuntime) + public HotKeyService(IJSRuntime jsRuntime, ILogger eventHandlerLogger) { JsRuntime = jsRuntime; + + HotKeyPressed = new(eventHandlerLogger); } public async Task RegisterHotkey(string key, string modifier, string action) diff --git a/Moonlight/Core/Services/IdentityService.cs b/Moonlight/Core/Services/IdentityService.cs deleted file mode 100644 index 4c5aec69..00000000 --- a/Moonlight/Core/Services/IdentityService.cs +++ /dev/null @@ -1,144 +0,0 @@ -using MoonCore.Abstractions; -using MoonCore.Attributes; -using MoonCore.Exceptions; -using MoonCore.Helpers; -using MoonCore.Services; -using Moonlight.Core.Configuration; -using Moonlight.Core.Database.Entities; -using Moonlight.Core.Models.Enums; - -namespace Moonlight.Core.Services; - -[Scoped] -public class IdentityService : IDisposable -{ - public User? CurrentUserNullable { get; private set; } - public User CurrentUser => CurrentUserNullable!; - public bool IsLoggedIn => CurrentUserNullable != null; - public SmartEventHandler OnAuthenticationStateChanged { get; set; } = new(); - - private string Token = ""; - - private readonly JwtService JwtService; - private readonly ConfigService ConfigService; - private readonly Repository UserRepository; - - public IdentityService( - JwtService jwtService, - ConfigService configService, - Repository userRepository) - { - JwtService = jwtService; - ConfigService = configService; - UserRepository = userRepository; - } - - public async Task Authenticate(User user) - { - var duration = TimeSpan.FromDays(ConfigService.Get().Authentication.TokenDuration); - - var token = await JwtService.Create(data => - { - data.Add("UserId", user.Id.ToString()); - data.Add("IssuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()); - }, CoreJwtType.Login, duration); - - await Authenticate(token); - - return token; - } - - public async Task Authenticate(string token) - { - Token = token; - - await Authenticate(); - } - - public async Task Authenticate(bool forceStateChange = false) // Can be used for authentication of the token as well - { - var lastUserId = CurrentUserNullable?.Id ?? -1; - - await ProcessToken(); - - var currentUserId = CurrentUserNullable?.Id ?? -1; - - if (lastUserId != currentUserId || forceStateChange) - await OnAuthenticationStateChanged.Invoke(); - } - - private async Task ProcessToken() - { - CurrentUserNullable = null; - - // Check jwt signature - if (!await JwtService.Validate(Token, CoreJwtType.Login)) - return; - - var data = await JwtService.Decode(Token); - - // Check for missing content - if(!data.ContainsKey("UserId") || !data.ContainsKey("IssuedAt")) - return; - - // Load user - var userId = int.Parse(data["UserId"]); - - var user = UserRepository - .Get() - .FirstOrDefault(x => x.Id == userId); - - // Check if user was found - if(user == null) - return; - - // Check token valid time - var issuedAt = long.Parse(data["IssuedAt"]); - var issuedAtDateTime = DateTimeOffset.FromUnixTimeSeconds(issuedAt).DateTime; - - // If the valid time is newer then when the token was issued, the token is not longer valid - if (user.TokenValidTimestamp > issuedAtDateTime) - return; - - CurrentUserNullable = user; - } - - public Task HasFlag(string flag) - { - if (!IsLoggedIn) - return Task.FromResult(false); - - var flags = CurrentUser.Flags.Split(";"); - - return Task.FromResult(flags.Contains(flag)); - } - - public Task SetFlag(string flag, bool toggle) - { - if (!IsLoggedIn) - throw new DisplayException("Unable to set flag while not logged in"); - - var flags = CurrentUser.Flags.Split(";").ToList(); - - if (toggle) - { - if(!flags.Contains(flag)) - flags.Add(flag); - } - else - { - if (flags.Contains(flag)) - flags.Remove(flag); - } - - CurrentUser.Flags = string.Join(';', flags); - UserRepository.Update(CurrentUser); - - return Task.CompletedTask; - } - - public async void Dispose() - { - await OnAuthenticationStateChanged.ClearSubscribers(); - } -} \ No newline at end of file diff --git a/Moonlight/Core/Services/MoonlightService.cs b/Moonlight/Core/Services/MoonlightService.cs index 7b434c1c..6aae4a4a 100644 --- a/Moonlight/Core/Services/MoonlightService.cs +++ b/Moonlight/Core/Services/MoonlightService.cs @@ -16,6 +16,14 @@ public class MoonlightService public WebApplication Application { get; set; } // Do NOT modify using a plugin private readonly DateTime StartTimestamp = DateTime.UtcNow; + private readonly CoreEvents CoreEvents; + private readonly ILogger Logger; + + public MoonlightService(CoreEvents coreEvents, ILogger logger) + { + CoreEvents = coreEvents; + Logger = logger; + } public MoonlightService() { @@ -50,7 +58,7 @@ public class MoonlightService public async Task Restart() { - Logger.Info("Restarting moonlight"); + Logger.LogInformation("Restarting moonlight"); // Notify all users that this instance will restart await CoreEvents.OnMoonlightRestart.Invoke(); diff --git a/Moonlight/Core/Services/PluginService.cs b/Moonlight/Core/Services/PluginService.cs index 20c66fec..af3bca87 100644 --- a/Moonlight/Core/Services/PluginService.cs +++ b/Moonlight/Core/Services/PluginService.cs @@ -9,8 +9,12 @@ public class PluginService private readonly Dictionary> ImplementationCache = new(); private readonly List Plugins = new(); - public PluginService() + private readonly ILogger Logger; + + public PluginService(ILogger logger) { + Logger = logger; + Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); } @@ -136,7 +140,7 @@ public class PluginService if (pluginTypes.Length == 0) { - Logger.Info($"Loaded assembly as library. {dllFile}"); + Logger.LogInformation("Loaded assembly as library: {dllFile}", dllFile); continue; } @@ -146,19 +150,17 @@ public class PluginService { var plugin = await LoadFromType(pluginType); - Logger.Info($"Loaded plugin '{plugin.Name}'. Created by '{plugin.Author}'"); + Logger.LogInformation("Loaded plugin '{name}'. Created by '{author}'", plugin.Name, plugin.Author); } catch (Exception e) { - Logger.Fatal($"An error occured while loading plugin '{pluginType.FullName}'"); - Logger.Fatal(e); + Logger.LogError("An error occured while loading plugin '{name}': {e}", pluginType.FullName, e); } } } catch (Exception e) { - Logger.Fatal($"An error occured while loading assembly '{dllFile}'"); - Logger.Fatal(e); + Logger.LogError("An error occured while loading assembly '{dllFile}': {e}", dllFile, e); } } } diff --git a/Moonlight/Core/Services/StartupJobService.cs b/Moonlight/Core/Services/StartupJobService.cs index 08b2579b..88f39ff5 100644 --- a/Moonlight/Core/Services/StartupJobService.cs +++ b/Moonlight/Core/Services/StartupJobService.cs @@ -9,10 +9,12 @@ public class StartupJobService : BackgroundService { private readonly List Jobs = new(); private readonly IServiceProvider ServiceProvider; + private readonly ILogger Logger; - public StartupJobService(IServiceProvider serviceProvider) + public StartupJobService(IServiceProvider serviceProvider, ILogger logger) { ServiceProvider = serviceProvider; + Logger = logger; } public Task AddJob(string name, TimeSpan delay, Func action) @@ -29,7 +31,7 @@ public class StartupJobService : BackgroundService public override Task Run() { - Logger.Info("Running startup jobs"); + Logger.LogInformation("Running startup jobs"); foreach (var job in Jobs) { @@ -42,8 +44,7 @@ public class StartupJobService : BackgroundService } catch (Exception e) { - Logger.Warn($"The startup job '{job.Name}' failed:"); - Logger.Warn(e); + Logger.LogWarning("The startup job '{name}' failed: {e}", job.Name, e); } }); } diff --git a/Moonlight/Core/Services/UnloadService.cs b/Moonlight/Core/Services/UnloadService.cs index d62ced48..dc1ab095 100644 --- a/Moonlight/Core/Services/UnloadService.cs +++ b/Moonlight/Core/Services/UnloadService.cs @@ -11,13 +11,17 @@ namespace Moonlight.Core.Services; [Scoped] public class UnloadService { - public SmartEventHandler OnUnloaded { get; set; } = new(); + public SmartEventHandler OnUnloaded { get; set; } private readonly IJSRuntime JsRuntime; + private readonly ILogger Logger; - public UnloadService(IJSRuntime jsRuntime) + public UnloadService(IJSRuntime jsRuntime, ILogger eventHandlerLogger, ILogger logger) { JsRuntime = jsRuntime; + Logger = logger; + + OnUnloaded = new(eventHandlerLogger); } public async Task Initialize() @@ -28,8 +32,7 @@ public class UnloadService } catch (Exception e) { - Logger.Error("An error occured while registering unload event handler"); - Logger.Error(e); + Logger.LogError("An error occured while registering unload event handler: {e}", e); } } diff --git a/Moonlight/Core/UI/Components/Auth/Login.razor b/Moonlight/Core/UI/Components/Auth/Login.razor index 64819a1a..87587c40 100644 --- a/Moonlight/Core/UI/Components/Auth/Login.razor +++ b/Moonlight/Core/UI/Components/Auth/Login.razor @@ -3,14 +3,14 @@ @using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Forms -@using Moonlight.Core.Services @using MoonCore.Exceptions -@using MoonCoreUI.Services +@using Moonlight.Core.Configuration @inject IAuthenticationProvider AuthenticationProvider @inject IdentityService IdentityService @inject CookieService CookieService @inject NavigationManager Navigation +@inject ConfigService ConfigService
@@ -24,7 +24,7 @@
- + @if (RequiresTwoFactor) {
@@ -58,7 +58,7 @@ @* OAuth2 Providers here *@
-
+ @@ -90,10 +90,13 @@ throw new DisplayException("A user with these credential combination was not found"); // Generate token and authenticate - var token = await IdentityService.Authenticate(user); + var token = await IdentityService.Login( + user.Id.ToString(), + TimeSpan.FromHours(ConfigService.Get().Authentication.TokenDuration) + ); // Save token for later use - await CookieService.SetValue("token", token); + await CookieService.SetValue("token", token, 30); //TODO: Add days to config option // Forward the user if not on the specific page if(new Uri(Navigation.Uri).LocalPath.StartsWith("/login")) diff --git a/Moonlight/Core/UI/Components/Auth/Register.razor b/Moonlight/Core/UI/Components/Auth/Register.razor index 03c6f9db..849b170f 100644 --- a/Moonlight/Core/UI/Components/Auth/Register.razor +++ b/Moonlight/Core/UI/Components/Auth/Register.razor @@ -4,10 +4,7 @@ @using Moonlight.Core.Models.Abstractions @using Moonlight.Core.Models.Forms -@using Moonlight.Core.Services @using MoonCore.Exceptions -@using MoonCore.Services -@using MoonCoreUI.Services @using Moonlight.Core.Configuration @inject IAuthenticationProvider AuthenticationProvider @@ -96,10 +93,13 @@ throw new DisplayException("Unable to create account"); // Generate token and authenticate - var token = await IdentityService.Authenticate(user); + var token = await IdentityService.Login( + user.Id.ToString(), + TimeSpan.FromHours(ConfigService.Get().Authentication.TokenDuration) + ); // Save token for later use - await CookieService.SetValue("token", token); + await CookieService.SetValue("token", token, 30); // TODO: config // Forward the user if not on the specific page if(new Uri(Navigation.Uri).LocalPath.StartsWith("/register")) diff --git a/Moonlight/Core/UI/Components/Auth/TwoFactorWizard.razor b/Moonlight/Core/UI/Components/Auth/TwoFactorWizard.razor index 701c54ab..1a9cda51 100644 --- a/Moonlight/Core/UI/Components/Auth/TwoFactorWizard.razor +++ b/Moonlight/Core/UI/Components/Auth/TwoFactorWizard.razor @@ -6,9 +6,10 @@ @using MoonCore.Exceptions @inject IdentityService IdentityService +@inject AlertService AlertService @inject IAuthenticationProvider AuthenticationProvider -@if (IdentityService.CurrentUser.Totp) +@if (IdentityService.GetUser().Totp) {
@@ -42,7 +43,7 @@ else var qrCodeData = qrGenerator.CreateQrCode ( - $"otpauth://totp/{Uri.EscapeDataString(IdentityService.CurrentUser.Email)}?secret={Key}&issuer={Uri.EscapeDataString("Moonlight")}", + $"otpauth://totp/{Uri.EscapeDataString(IdentityService.GetUser().Email)}?secret={Key}&issuer={Uri.EscapeDataString("Moonlight")}", QRCodeGenerator.ECCLevel.Q ); @@ -61,15 +62,15 @@ else
- +
- +
- +
} else @@ -139,10 +140,10 @@ else private string Key = ""; private TwoFactorCodeForm CodeForm = new(); - private async Task Disable() + private async Task Disable(ConfirmButton _) { - await AuthenticationProvider.SetTwoFactorSecret(IdentityService.CurrentUser, ""); - await IdentityService.Authenticate(true); + await AuthenticationProvider.SetTwoFactorSecret(IdentityService.GetUser(), ""); + await IdentityService.Authenticate(); HasStarted = false; HasCompletedAppLinks = false; @@ -170,11 +171,14 @@ else var correctCode = totp.ComputeTotp(); if (CodeForm.Code != correctCode) - throw new DisplayException("Invalid code entered. Please try again"); + { + await AlertService.Danger("Invalid code entered. Please try again"); + return; + } // Enable two factor auth for user - await AuthenticationProvider.SetTwoFactorSecret(IdentityService.CurrentUser, Key); - await IdentityService.Authenticate(true); + await AuthenticationProvider.SetTwoFactorSecret(IdentityService.GetUser(), Key); + await IdentityService.Authenticate(); HasStarted = false; HasCompletedAppLinks = false; diff --git a/Moonlight/Core/UI/Components/Cards/TimeBasedGreetingMessages.razor b/Moonlight/Core/UI/Components/Cards/TimeBasedGreetingMessages.razor index 0d62e63f..c645228f 100644 --- a/Moonlight/Core/UI/Components/Cards/TimeBasedGreetingMessages.razor +++ b/Moonlight/Core/UI/Components/Cards/TimeBasedGreetingMessages.razor @@ -1,6 +1,6 @@ @using MoonCore.Services @using Moonlight.Core.Configuration -@using Moonlight.Core.Services +@using Moonlight.Core.Database.Entities @inject IdentityService IdentityService @inject ConfigService ConfigService @@ -12,7 +12,7 @@ var greeting = GetGreetingMessage(); } @greeting.Item1 - @IdentityService.CurrentUser.Username + @IdentityService.GetUser().Username @greeting.Item2 diff --git a/Moonlight/Core/UI/Components/Partials/AppHeader.razor b/Moonlight/Core/UI/Components/Partials/AppHeader.razor index 930c4bc3..cf2d4e2f 100644 --- a/Moonlight/Core/UI/Components/Partials/AppHeader.razor +++ b/Moonlight/Core/UI/Components/Partials/AppHeader.razor @@ -1,7 +1,4 @@ -@using Moonlight.Core.Services -@using MoonCoreUI.Services - -@inject IdentityService IdentityService +@inject IdentityService IdentityService @inject CookieService CookieService
@@ -25,13 +22,13 @@
- @if (IdentityService.IsLoggedIn) + @if (IdentityService.IsAuthenticated) {
@@ -81,7 +78,7 @@ protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - IdentityService.OnAuthenticationStateChanged += OnAuthenticationStateChanged; + IdentityService.OnStateChanged += OnAuthenticationStateChanged; return Task.CompletedTask; } @@ -94,9 +91,9 @@ private async Task Logout() { // Reset token - await CookieService.SetValue("token", ""); + await CookieService.SetValue("token", "", 30); // Reset token in identity service - await IdentityService.Authenticate(""); + await IdentityService.Logout(); } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Components/Partials/AppSidebar.razor b/Moonlight/Core/UI/Components/Partials/AppSidebar.razor index ed251ff0..9341ed66 100644 --- a/Moonlight/Core/UI/Components/Partials/AppSidebar.razor +++ b/Moonlight/Core/UI/Components/Partials/AppSidebar.razor @@ -56,13 +56,13 @@ } } - @if (IdentityService.IsLoggedIn && IdentityService.CurrentUser.Permissions > 0) + @if (IdentityService.IsAuthenticated && IdentityService.GetUser().Permissions > 0) { - @foreach (var sidebarItem in FeatureService.UiContext.SidebarItems.Where(x => x.IsAdmin).OrderBy(x => x.Index).ToArray()) + foreach (var sidebarItem in FeatureService.UiContext.SidebarItems.Where(x => x.IsAdmin).OrderBy(x => x.Index).ToArray()) { if (IsMatch(sidebarItem)) { diff --git a/Moonlight/Core/UI/Components/Partials/CookieConsentBanner.razor b/Moonlight/Core/UI/Components/Partials/CookieConsentBanner.razor index f6b4b00e..c4885f78 100644 --- a/Moonlight/Core/UI/Components/Partials/CookieConsentBanner.razor +++ b/Moonlight/Core/UI/Components/Partials/CookieConsentBanner.razor @@ -1,7 +1,4 @@ -@using ApexCharts -@using MoonCore.Services -@using Moonlight.Core.Configuration -@using Moonlight.Core.Services +@using Moonlight.Core.Configuration @inject ConfigService ConfigService @inject IdentityService IdentityService @@ -12,17 +9,17 @@
-

@ConfigService.Get().Customisation.CookieConsentBanner.BannerTitle

+

@BannerTitle

- @ConfigService.Get().Customisation.CookieConsentBanner.BannerText + @BannerText

- - @ConfigService.Get().Customisation.CookieConsentBanner.ConsentText - - - @ConfigService.Get().Customisation.CookieConsentBanner.DeclineText - + + @ConsentText + + + @DeclineText +
@@ -30,16 +27,31 @@
} -@code { - +@code +{ private bool ShowBanner; + private string BannerTitle; + private string BannerText; + private string ConsentText; + private string DeclineText; + + protected override void OnInitialized() + { + var config = ConfigService.Get().Customisation.CookieConsentBanner; + + BannerTitle = config.BannerTitle; + BannerText = config.BannerText; + ConsentText = config.ConsentText; + DeclineText = config.DeclineText; + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var userWasAsked = await IdentityService.HasFlag("CookieAsked"); - + if (ConfigService.Get().Customisation.CookieConsentBanner.Enabled && !userWasAsked) ShowBanner = true; @@ -47,25 +59,16 @@ } } - private async Task Consent() + private async Task SetAnswer(bool answer) { - if (!IdentityService.IsLoggedIn) - return; - - await IdentityService.SetFlag("CookieAsked", true); - await IdentityService.SetFlag("CookieConsent", true); - - await InvokeAsync(StateHasChanged); - } - - private async Task Decline() - { - if (!IdentityService.IsLoggedIn) + if (!IdentityService.IsAuthenticated) return; await IdentityService.SetFlag("CookieAsked", true); - + + if (answer) + await IdentityService.SetFlag("CookieConsent", true); + await InvokeAsync(StateHasChanged); } - } \ No newline at end of file diff --git a/Moonlight/Core/UI/Components/Partials/PermissionChecker.razor b/Moonlight/Core/UI/Components/Partials/PermissionChecker.razor index ad21c235..ef404acf 100644 --- a/Moonlight/Core/UI/Components/Partials/PermissionChecker.razor +++ b/Moonlight/Core/UI/Components/Partials/PermissionChecker.razor @@ -52,7 +52,7 @@ else if(permissionRequired == null) continue; - if (IdentityService.CurrentUser.Permissions >= permissionRequired.Level) + if (IdentityService.GetUser().Permissions >= permissionRequired.Level) Allowed = true; } diff --git a/Moonlight/Core/UI/Components/Partials/SoftErrorHandler.razor b/Moonlight/Core/UI/Components/Partials/SoftErrorHandler.razor index f6c4400c..ff596554 100644 --- a/Moonlight/Core/UI/Components/Partials/SoftErrorHandler.razor +++ b/Moonlight/Core/UI/Components/Partials/SoftErrorHandler.razor @@ -4,12 +4,13 @@ @using Moonlight.Core.Services @inject IdentityService IdentityService +@inject ILogger Logger @inherits ErrorBoundaryBase @if (Crashed || Exception != null) { - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || (IdentityService.IsLoggedIn && IdentityService.CurrentUser.Permissions >= 9000)) + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || (IdentityService.IsAuthenticated && IdentityService.GetUser().Permissions >= 9000)) { if (Exception != null) { @@ -74,9 +75,8 @@ else Exception = exception; Crashed = true; - var username = IdentityService.IsLoggedIn ? IdentityService.CurrentUser.Username : "Guest"; - Logger.Warn($"A crash occured in the view of '{username}'"); - Logger.Warn(exception); + var username = IdentityService.IsAuthenticated ? IdentityService.GetUser().Username : "Guest"; + Logger.LogWarning("A crash occured in the view of '{username}': {exception}", username, exception); } Recover(); diff --git a/Moonlight/Core/UI/Layouts/MainLayout.razor b/Moonlight/Core/UI/Layouts/MainLayout.razor index f0007150..5ad40a44 100644 --- a/Moonlight/Core/UI/Layouts/MainLayout.razor +++ b/Moonlight/Core/UI/Layouts/MainLayout.razor @@ -1,6 +1,5 @@ @using Moonlight.Core.Services @using Moonlight.Core.UI.Components.Auth -@using MoonCore.Services @using Moonlight.Core.Configuration @using Moonlight.Core.Events @@ -13,6 +12,7 @@ @inject UnloadService UnloadService @inject ConfigService ConfigService @inject ScopedStorageService ScopedStorageService +@inject CoreEvents CoreEvents @implements IDisposable @@ -25,7 +25,7 @@
- @if (IdentityService.IsLoggedIn) + @if (IdentityService.IsAuthenticated) { } @@ -52,7 +52,7 @@ { if (IsInitialized) { - if (IdentityService.IsLoggedIn) + if (IdentityService.IsAuthenticated) { @Body @@ -76,9 +76,9 @@
- - - +
@@ -99,6 +99,9 @@
+ + + @code { private bool IsInitialized = false; @@ -110,7 +113,7 @@ // Base init await lazyLoader.SetText("Initializing"); - IdentityService.OnAuthenticationStateChanged += OnAuthenticationStateChanged; + IdentityService.OnStateChanged += OnAuthenticationStateChanged; CoreEvents.OnMoonlightRestart += async () => { diff --git a/Moonlight/Core/UI/Views/Account/Api.razor b/Moonlight/Core/UI/Views/Account/Api.razor index fb21b19d..cc5bbc81 100644 --- a/Moonlight/Core/UI/Views/Account/Api.razor +++ b/Moonlight/Core/UI/Views/Account/Api.razor @@ -10,7 +10,7 @@ @* the @@@ looks weird, ik that, it will result in @username *@ - @@@IdentityService.CurrentUser.Username + @@@IdentityService.GetUser().Username diff --git a/Moonlight/Core/UI/Views/Account/Index.razor b/Moonlight/Core/UI/Views/Account/Index.razor index 30963c15..73b39638 100644 --- a/Moonlight/Core/UI/Views/Account/Index.razor +++ b/Moonlight/Core/UI/Views/Account/Index.razor @@ -1,9 +1,10 @@ @page "/account" +@using System.ComponentModel.DataAnnotations @using Moonlight.Core.Services @using Moonlight.Core.Models.Forms @using Mappy.Net -@using MoonCoreUI.Services +@using Moonlight.Core.Database.Entities @using Moonlight.Core.Models.Abstractions @using Moonlight.Core.UI.Components.Navigations @@ -16,63 +17,83 @@ @* the @@@ looks weird, ik that, it will result in @username *@ - @@@IdentityService.CurrentUser.Username + @@@IdentityService.GetUser().Username - - -
-
-
-
-
-
- image -
-
-
- To change your profile picture go to Gravatar and - register with the same email address you are using here +
+
+
+
+
+
+ image
+
+ To change your profile picture go to Gravatar and + register with the same email address you are using here +
-
-
- -
- -
- -
+
+
+
+
+ +
+
- +
@code { - private UpdateAccountForm Form = new(); + private UpdateAccountForm Model; + private FastForm Form; - private Task Load(LazyLoader _) + protected override void OnInitialized() { - Form = Mapper.Map(IdentityService.CurrentUser); - - return Task.CompletedTask; + // Create a copy of the user + Model = Mapper.Map(IdentityService.GetUser()); } - private async Task Update() + private void OnConfigure(FastFormConfiguration configuration) { - await AuthenticationProvider.ChangeDetails(IdentityService.CurrentUser, Form.Email, Form.Username); + configuration.AddProperty(x => x.Username) + .WithComponent(component => + { + component.ColumnsMd = 12; + }) + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number")) + .WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new("The username is too short")) + .WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new("The username cannot be longer than 20 characters")); + + configuration.AddProperty(x => x.Email) + .WithComponent(component => + { + component.ColumnsMd = 12; + component.Type = "email"; + }) + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^.+@.+$", "You need to provide a valid email address")); + } + + private async Task SaveChanges() + { + if(!await Form.Submit()) + return; + + await AuthenticationProvider.ChangeDetails(IdentityService.GetUser(), Model.Email, Model.Username); await ToastService.Success("Successfully updated details"); // This will trigger a re-render as well as an update of the model - await IdentityService.Authenticate(true); + await IdentityService.Authenticate(); } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Account/Security.razor b/Moonlight/Core/UI/Views/Account/Security.razor index 7c2cb050..a3ebfabd 100644 --- a/Moonlight/Core/UI/Views/Account/Security.razor +++ b/Moonlight/Core/UI/Views/Account/Security.razor @@ -1,9 +1,9 @@ @page "/account/security" +@using System.ComponentModel.DataAnnotations @using Moonlight.Core.Services @using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.Models.Forms -@using MoonCoreUI.Services @using Moonlight.Core.Models.Abstractions @using MoonCore.Exceptions @using Moonlight.Core.UI.Components.Auth @@ -17,81 +17,104 @@ @* the @@@ looks weird, ik that, it will result in @username *@ - @@@IdentityService.CurrentUser.Username + @@@IdentityService.GetUser().Username
- -
-
-
- -
-
- -
-
- -
+
+
+
+
+
-
-
-
-
- -
-
-
-
-
- -
-

Cookies

-
- - -
-
- -
+
- +
+
+
+ +
+
+
+
+
+
+

Cookies

+

+ This specifies if you would like to personalize your experience with optional cookies. +

+
+ @if (CookieConsent) + { + + } + else + { + + } +
+
+
+
+
@code { - private readonly ChangePasswordForm PasswordForm = new(); - - private ChangeCookiesForm CookiesForm = new(); + private ChangePasswordForm PasswordModel = new(); + private FastForm PasswordForm; - private async Task Load(LazyLoader lazyLoader) + private bool CookieConsent; + + protected override async Task OnInitializedAsync() { - CookiesForm.UseOptionalCookies = await IdentityService.HasFlag("CookieConsent"); + CookieConsent = await IdentityService.HasFlag("CookieConsent"); } - private async Task OnValidSubmitPassword() + private async Task SetCookieConsent(bool flag) { - if (PasswordForm.Password != PasswordForm.RepeatedPassword) - throw new DisplayException("The passwords do not match"); + await IdentityService.SetFlag("CookieConsent", flag); - await AuthenticationProvider.ChangePassword(IdentityService.CurrentUser, PasswordForm.Password); - - await ToastService.Success("Successfully changed password"); - await IdentityService.Authenticate(true); - } - - private async Task OnValidSubmitCookie() - { - await IdentityService.SetFlag("CookieConsent", CookiesForm.UseOptionalCookies); - + await ToastService.Success("Successfully changed cookie preferences"); await InvokeAsync(StateHasChanged); } + + private void OnConfigurePasswordForm(FastFormConfiguration configuration) + { + configuration.AddProperty(x => x.Password) + .WithComponent(component => + { + component.ColumnsMd = 6; + component.Type = "password"; + }) + .WithValidation(FastFormValidators.Required) + .WithValidation(x => x.Length >= 8 ? ValidationResult.Success : new("The password must be at least 8 characters long")) + .WithValidation(x => x.Length <= 256 ? ValidationResult.Success : new("The password must not be longer than 256 characters")); + + configuration.AddProperty(x => x.RepeatedPassword) + .WithComponent(component => + { + component.ColumnsMd = 6; + component.Type = "password"; + }); + } + + private async Task ChangePassword() + { + if(!await PasswordForm.Submit()) + return; + + if (PasswordModel.Password != PasswordModel.RepeatedPassword) + throw new DisplayException("The passwords do not match"); + + await AuthenticationProvider.ChangePassword(IdentityService.GetUser(), PasswordModel.Password); + + await ToastService.Success("Successfully changed password"); + await IdentityService.Authenticate(); + } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Admin/Api/Index.razor b/Moonlight/Core/UI/Views/Admin/Api/Index.razor index 06d66566..16b166e2 100644 --- a/Moonlight/Core/UI/Views/Admin/Api/Index.razor +++ b/Moonlight/Core/UI/Views/Admin/Api/Index.razor @@ -21,11 +21,11 @@
- - - - - + + + + + - - + +
diff --git a/Moonlight/Core/UI/Views/Admin/Api/Keys.razor b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor index 79223d13..d88630ca 100644 --- a/Moonlight/Core/UI/Views/Admin/Api/Keys.razor +++ b/Moonlight/Core/UI/Views/Admin/Api/Keys.razor @@ -1,10 +1,8 @@ @page "/admin/api/keys" -@using MoonCore.Abstractions +@using MoonCore.Blazor.Models.FastForms @using MoonCore.Helpers -@using MoonCoreUI.Services @using Moonlight.Core.Database.Entities -@using Moonlight.Core.Models.Forms.ApiKeys @using Moonlight.Core.UI.Components.Navigations @inject ClipboardService ClipboardService @@ -15,17 +13,15 @@
- + - + - - - + + + - - + + - - + + - +
@code { - private IEnumerable ApiKeysLoader(Repository repository) + private void OnConfigure(FastCrudConfiguration configuration) { - return repository.Get(); + configuration.ValidateCreate = async apiKey => + { + // TODO: Remove this when correct permission editor exists + if (string.IsNullOrEmpty(apiKey.PermissionJson)) + apiKey.PermissionJson = "[]"; + + var key = Formatter.GenerateString(32); + apiKey.Key = key; + + await ClipboardService.Copy(key); + await ToastService.Info("Copied api key into your clipboard"); + }; } - private async Task ValidateAdd(ApiKey apiKey) + private void OnConfigureFrom(FastFormConfiguration configuration, ApiKey _) { - var key = Formatter.GenerateString(32); - apiKey.Key = key; + configuration.AddProperty(x => x.Description) + .WithDefaultComponent() + .WithDescription("Write a note here for which application the api key is used for") + .WithValidation(FastFormValidators.Required); - await ClipboardService.Copy(key); - await ToastService.Info("Copied api key into your clipboard"); + configuration.AddProperty(x => x.ExpiresAt) + .WithDefaultComponent() + .WithDescription("Specify when the api key should expire"); + + configuration.AddProperty(x => x.PermissionJson) + .WithDefaultComponent() + .WithName("Permissions"); } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Admin/Index.razor b/Moonlight/Core/UI/Views/Admin/Index.razor index 9d7d9a22..872cf906 100644 --- a/Moonlight/Core/UI/Views/Admin/Index.razor +++ b/Moonlight/Core/UI/Views/Admin/Index.razor @@ -12,7 +12,7 @@
@foreach (var column in Columns.OrderBy(x => x.Index)) { - if (column.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) + if (column.RequiredPermissionLevel <= IdentityService.GetUser().Permissions) {
@column.Component @@ -22,7 +22,7 @@
@foreach (var component in Components.OrderBy(x => x.Index)) { - if (component.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) + if (component.RequiredPermissionLevel <= IdentityService.GetUser().Permissions) {
@component.Component diff --git a/Moonlight/Core/UI/Views/Admin/Sys/Diagnose.razor b/Moonlight/Core/UI/Views/Admin/Sys/Diagnose.razor index 5a0fcad7..c13d45f7 100644 --- a/Moonlight/Core/UI/Views/Admin/Sys/Diagnose.razor +++ b/Moonlight/Core/UI/Views/Admin/Sys/Diagnose.razor @@ -1,10 +1,9 @@ @page "/admin/sys/diagnose" @using Moonlight.Core.UI.Components.Navigations -@using MoonCoreUI.Services @using Moonlight.Core.Services -@inject FileDownloadService DownloadService +@inject DownloadService DownloadService @inject DiagnoseService DiagnoseService @attribute [RequirePermission(9999)] @@ -31,7 +30,7 @@
- + Download diagnose report
diff --git a/Moonlight/Core/UI/Views/Admin/Sys/Index.razor b/Moonlight/Core/UI/Views/Admin/Sys/Index.razor index 0c88b743..12ba8a75 100644 --- a/Moonlight/Core/UI/Views/Admin/Sys/Index.razor +++ b/Moonlight/Core/UI/Views/Admin/Sys/Index.razor @@ -6,6 +6,7 @@ @using MoonCore.Helpers @inject MoonlightService MoonlightService +@inject HostSystemHelper HostSystemHelper @attribute [RequirePermission(9999)] @@ -75,7 +76,7 @@
- + Restart
diff --git a/Moonlight/Core/UI/Views/Admin/Sys/Permissions.razor b/Moonlight/Core/UI/Views/Admin/Sys/Permissions.razor index 31603fd8..ded39571 100644 --- a/Moonlight/Core/UI/Views/Admin/Sys/Permissions.razor +++ b/Moonlight/Core/UI/Views/Admin/Sys/Permissions.razor @@ -2,7 +2,6 @@ @using Moonlight.Core.UI.Components.Navigations @using MoonCore.Abstractions -@using MoonCoreUI.Services @using Moonlight.Core.Database.Entities @using Moonlight.Core.Services @@ -17,7 +16,7 @@
-
- @if (SelectedUser == null) - { - - } - else - { - - } + Apply
} diff --git a/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor b/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor index cacd51dd..11a925dc 100644 --- a/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor +++ b/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor @@ -1,10 +1,10 @@ @page "/admin/sys/settings" +@using System.ComponentModel +@using System.Linq.Expressions @using Moonlight.Core.UI.Components.Navigations @using System.Reflection @using MoonCore.Services -@using MoonCoreUI.Services -@using MoonCoreUI.Helpers @using Moonlight.Core.Configuration @inject ConfigService ConfigService @@ -12,9 +12,9 @@ @attribute [RequirePermission(9999)] - + -@if (ModelToShow == null) +@if (CurrentModel == null) { No model found to show. Please refresh the page to go back @@ -22,20 +22,36 @@ } else { +
+ + Changes to these settings are live applied. The save button only make the changes persistently saved to disk + +
+
- @{ - string title; - - if (Path.Length == 0) - title = "Configuration"; +

+ @if (Path.Length == 0) + { + Configuration + } else { - title = "Configuration - " + string.Join(" - ", Path); + + Configuration + + @foreach (var subPart in Path.SkipLast(1)) + { + + @subPart + } + + + + @Path.Last() + } - } - -

@(title)

+
@@ -46,34 +62,24 @@ else
- - - Changes to these settings are live applied. The save button only make the changes persistently saved to disk - - +
- @{ - var props = ModelToShow - .GetType() - .GetProperties() - .Where(x => x.PropertyType.Assembly.FullName!.Contains("Moonlight") && x.PropertyType.IsClass) - .ToArray(); - } - - @foreach (var prop in props) + @foreach (var item in SidebarItems) { } @if (Path.Length != 0) { -
+
@@ -82,40 +88,21 @@ else
-
-
- - @foreach (var prop in Properties) - { -
- @{ - var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType); - var rf = ComponentHelper.FromType(typeToCreate, parameters => - { - parameters.Add("Data", ModelToShow); - parameters.Add("Property", prop); - }); - } - - @rf -
- } -
-
-
+ + +
} @code { - [Parameter] - [SupplyParameterFromQuery] - public string? Section { get; set; } = ""; - - private object? ModelToShow; - private PropertyInfo[] Properties = Array.Empty(); - private string[] Path = Array.Empty(); + [Parameter] [SupplyParameterFromQuery] public string? Section { get; set; } = ""; + + private object? CurrentModel; + private string[] SidebarItems = []; + private string[] Path = []; + private PropertyInfo[] Properties = []; private LazyLoader? LazyLoader; @@ -124,21 +111,36 @@ else if (Section != null && Section.StartsWith("/")) Section = Section.TrimStart('/'); - Path = Section != null ? Section.Split("/") : Array.Empty(); + Path = Section != null ? Section.Split("/") : []; - ModelToShow = Resolve(ConfigService.Get(), Path, 0); + CurrentModel = Resolve(ConfigService.Get(), Path, 0); - if (ModelToShow != null) + if (CurrentModel == null) { - Properties = ModelToShow - .GetType() - .GetProperties() - .Where(x => !x.PropertyType.Assembly.FullName!.Contains("Moonlight")) - .ToArray(); + SidebarItems = []; + Properties = []; } else { - Properties = Array.Empty(); + var props = CurrentModel + .GetType() + .GetProperties() + .ToArray(); + + SidebarItems = props + .Where(x => + x.PropertyType.IsClass && + x.PropertyType.Namespace!.StartsWith("Moonlight") + ) + .Select(x => x.Name) + .ToArray(); + + Properties = props + .Where(x => + !x.PropertyType.Namespace.StartsWith("Moonlight") && + DefaultComponentRegistry.Get(x.PropertyType) != null // Check if a component has been registered for that type + ) + .ToArray(); } await InvokeAsync(StateHasChanged); @@ -146,16 +148,38 @@ else if (LazyLoader != null) await LazyLoader.Reload(); } + + private void OnFormConfigure(FastFormConfiguration configuration) + { + if(CurrentModel == null) // This will technically never be true because of the ui logic + return; + + foreach (var property in Properties) + { + var propConfig = configuration + .AddProperty(CreatePropertyAccessExpression(property)) + .WithDefaultComponent(); + + var customAttributes = property.GetCustomAttributes(false); + + if(customAttributes.Length == 0) + continue; + + if (TryGetAttribute(customAttributes, out DisplayNameAttribute nameAttribute)) + propConfig.WithName(nameAttribute.DisplayName); + + if (TryGetAttribute(customAttributes, out DescriptionAttribute descriptionAttribute)) + propConfig.WithDescription(descriptionAttribute.Description); + } + } private string GetBackPath() { if (Path.Length == 1) return "settings"; - else - { - var path = string.Join('/', Path.Take(Path.Length - 1)).TrimEnd('/'); - return $"settings?section={path}"; - } + + var path = string.Join('/', Path.Take(Path.Length - 1)).TrimEnd('/'); + return $"settings?section={path}"; } private object? Resolve(object model, string[] path, int index) @@ -177,20 +201,60 @@ else return Resolve(prop.GetValue(model)!, path, index + 1); } - private Task Load(LazyLoader arg) - { - return Task.CompletedTask; - } + private Task Load(LazyLoader _) => Task.CompletedTask; // Seems useless, it more or less is, but it shows a nice loading ui while the form changes - private async Task Save() + private async Task Save() // Saves all changes to disk, all changes are live updated as the config service reference will be edited directly { ConfigService.Save(); await ToastService.Success("Successfully saved config to disk"); } - private async Task Reload() + private async Task Reload() // This will also discard all unsaved changes { ConfigService.Reload(); await ToastService.Info("Reloaded configuration from disk"); } + + // Building lambda expressions at runtime using reflection is nice ;3 + public static Expression> CreatePropertyAccessExpression(PropertyInfo property) + { + // Get the type that declares the property + Type declaringType = property.DeclaringType!; + + // Create a parameter expression for the input object + ParameterExpression param = Expression.Parameter(typeof(object), "obj"); + + // Create an expression to cast the input object to the declaring type + UnaryExpression cast = Expression.Convert(param, declaringType); + + // Create an expression to access the property + MemberExpression propertyAccess = Expression.Property(cast, property); + + // Create an expression to cast the property value to object + UnaryExpression castResult = Expression.Convert(propertyAccess, typeof(object)); + + // Create the final lambda expression + Expression> lambda = Expression.Lambda>( + castResult, param); + + return lambda; + } + + // From MoonCore. TODO: Maybe provide this and the above function as mooncore helper + private bool TryGetAttribute(object[] attributes, out T result) where T : Attribute + { + var searchType = typeof(T); + + var attr = attributes + .FirstOrDefault(x => x.GetType() == searchType); + + if (attr == null) + { + result = default!; + return false; + } + + result = (attr as T)!; + return true; + } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Admin/Users/Index.razor b/Moonlight/Core/UI/Views/Admin/Users/Index.razor index 106923bd..0bb30809 100644 --- a/Moonlight/Core/UI/Views/Admin/Users/Index.razor +++ b/Moonlight/Core/UI/Views/Admin/Users/Index.razor @@ -1,13 +1,10 @@ @page "/admin/users" +@using System.ComponentModel.DataAnnotations @using Moonlight.Core.UI.Components.Navigations -@using MoonCore.Abstractions @using Moonlight.Core.Database.Entities -@using BlazorTable @using MoonCore.Exceptions -@using MoonCoreUI.Services @using Moonlight.Core.Models.Abstractions -@using Moonlight.Core.Models.Forms.Users @inject AlertService AlertService @inject IAuthenticationProvider AuthenticationProvider @@ -16,55 +13,98 @@ - + - - - - + + + + - + Change password - - + + @code { - private IEnumerable Load(Repository repository) - { - return repository.Get(); - } - private async Task ChangePassword(User user) { - var newPassword = await AlertService.Text($"Enter a new password for {user.Username}", ""); + await AlertService.Text($"Change password for '{user.Username}'", "Enter a new password for {user.Username}", async newPassword => + { + // This handles empty and canceled input + if (string.IsNullOrEmpty(newPassword)) + return; - // This handles empty and canceled input - if (string.IsNullOrEmpty(newPassword)) - return; + await AuthenticationProvider.ChangePassword(user, newPassword); + }); + } + + private void OnConfigure(FastCrudConfiguration configuration) + { + configuration.CustomCreate = async user => + { + var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); - await AuthenticationProvider.ChangePassword(user, newPassword); + if (result == null) + throw new DisplayException("An unknown error occured while creating user"); + }; + + configuration.ValidateEdit = async user => + { + await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username); + }; } - private async Task Add(User user) + private void OnConfigureCreate(FastFormConfiguration configuration, User _) { - var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); + configuration.AddProperty(x => x.Username) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number")) + .WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short")) + .WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new ValidationResult("The username cannot be longer than 20 characters")); - if (result == null) - throw new DisplayException("An unknown error occured while creating user"); + configuration.AddProperty(x => x.Email) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^.+@.+$", "You need to enter a valid email address")); + + configuration.AddProperty(x => x.Password) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(x => x.Length >= 8 ? ValidationResult.Success : new ValidationResult("The password must be at least 8 characters long")) + .WithValidation(x => x.Length <= 256 ? ValidationResult.Success : new ValidationResult("The password must not be longer than 256 characters")); } - // To notify the authentication provider before we update the data in the database, we call it here - private async Task ValidateUpdate(User user) + private void OnConfigureEdit(FastFormConfiguration configuration, User currentUser) { - await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username); + configuration.AddProperty(x => x.Username) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^[a-z][a-z0-9]*$", "Usernames can only contain lowercase characters and numbers and should not start with a number")) + .WithValidation(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short")) + .WithValidation(x => x.Length <= 20 ? ValidationResult.Success : new ValidationResult("The username cannot be longer than 20 characters")); + + configuration.AddProperty(x => x.Email) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^.+@.+$", "You need to enter a valid email address")); + + configuration.AddProperty(x => x.Totp) + .WithComponent() + .WithName("Two factor authentication") + .WithDescription("This toggles the use of the two factor authentication"); + } + + private IEnumerable Search(IEnumerable source, string term) + { + return source.Where(x => x.Username.Contains(term) || x.Email.Contains(term)); } } \ No newline at end of file diff --git a/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor b/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor index 9d626380..faf93bad 100644 --- a/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor +++ b/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor @@ -3,7 +3,6 @@ @using Moonlight.Core.UI.Components.Navigations @using Moonlight.Core.Services @using MoonCore.Helpers -@using MoonCoreUI.Services @using Moonlight.Core.Models @inject SessionService SessionService @@ -24,16 +23,18 @@
- - + + - - + + - - + + - - + + - - + + - - + +
@@ -77,7 +78,7 @@ @code { - private CrudTable? Table; + private MCBTable? Table; private Timer? UpdateTimer; private Task Load(LazyLoader _) @@ -93,40 +94,42 @@ private async Task Redirect(Session session) { - var url = await AlertService.Text("Enter the target url to redirect to"); - - if (string.IsNullOrEmpty(url)) - return; - - try + await AlertService.Text("Redirect to", "Enter the target url to redirect to", async url => { - session.NavigationManager.NavigateTo(url); + if (string.IsNullOrEmpty(url)) + return; - await ToastService.Success("Successfully redirected user session"); - } - catch (Exception) - { - await ToastService.Danger("Unable to redirect user. The user is probably no longer connect with moonlight"); - } + try + { + session.NavigationManager.NavigateTo(url); + + await ToastService.Success("Successfully redirected user session"); + } + catch (Exception) + { + await ToastService.Danger("Unable to redirect user. The user is probably no longer connect with moonlight"); + } + }); } private async Task Message(Session session) { - var message = await AlertService.Text("Enter the message you want to send"); - - if (string.IsNullOrEmpty(message)) - return; - - try + await AlertService.Text("Send message", "Enter the message you want to send", async message => { - await session.AlertService.Info(message); + if (string.IsNullOrEmpty(message)) + return; - await ToastService.Success("Successfully sent message to user session"); - } - catch (Exception) - { - await ToastService.Danger("Unable to send message. The user is probably no longer connect with moonlight"); - } + try + { + await session.AlertService.Info(message); + + await ToastService.Success("Successfully sent message to user session"); + } + catch (Exception) + { + await ToastService.Danger("Unable to send message. The user is probably no longer connect with moonlight"); + } + }); } public void Dispose() diff --git a/Moonlight/Core/UI/Views/Index.razor b/Moonlight/Core/UI/Views/Index.razor index acef82a3..fa5574c8 100644 --- a/Moonlight/Core/UI/Views/Index.razor +++ b/Moonlight/Core/UI/Views/Index.razor @@ -9,7 +9,7 @@ @foreach (var component in Components.OrderBy(x => x.Index)) { - if (component.RequiredPermissionLevel <= IdentityService.CurrentUser.Permissions) + if (component.RequiredPermissionLevel <= IdentityService.GetUser().Permissions) {
@component.Component diff --git a/Moonlight/Features/FileManager/FileManagerFeature.cs b/Moonlight/Features/FileManager/FileManagerFeature.cs index eb80a640..d0e56c7b 100644 --- a/Moonlight/Features/FileManager/FileManagerFeature.cs +++ b/Moonlight/Features/FileManager/FileManagerFeature.cs @@ -24,7 +24,13 @@ public class FileManagerFeature : MoonlightFeature // var config = new ConfigService(PathBuilder.File("storage", "configs", "core.json")); - context.Builder.Services.AddSingleton(new JwtService(config.Get().Security.Token)); + + context.Builder.Services.AddSingleton( + new JwtService( + config.Get().Security.Token, + context.LoggerFactory.CreateLogger>() + ) + ); context.AddAsset("FileManager", "js/filemanager.js"); context.AddAsset("FileManager", "editor/ace.css"); diff --git a/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs b/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs index 217f30fa..d73b54b6 100644 --- a/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs +++ b/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs @@ -13,11 +13,13 @@ public class DownloadController : Controller { private readonly JwtService JwtService; private readonly SharedFileAccessService SharedFileAccessService; + private readonly ILogger Logger; - public DownloadController(JwtService jwtService, SharedFileAccessService sharedFileAccessService) + public DownloadController(JwtService jwtService, SharedFileAccessService sharedFileAccessService, ILogger logger) { JwtService = jwtService; SharedFileAccessService = sharedFileAccessService; + Logger = logger; } [HttpGet] @@ -25,7 +27,7 @@ public class DownloadController : Controller { if (name.Contains("..")) { - Logger.Warn($"A user tried to access a file via path transversal. Name: {name}"); + Logger.LogWarning("A user tried to access a file via path transversal. Name: {name}", name); return NotFound(); } diff --git a/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs b/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs index 93255e0b..37b9abe2 100644 --- a/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs +++ b/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs @@ -13,13 +13,15 @@ public class UploadController : Controller { private readonly JwtService JwtService; private readonly SharedFileAccessService SharedFileAccessService; + private readonly ILogger Logger; public UploadController( JwtService jwtService, - SharedFileAccessService sharedFileAccessService) + SharedFileAccessService sharedFileAccessService, ILogger logger) { JwtService = jwtService; SharedFileAccessService = sharedFileAccessService; + Logger = logger; } // The following method/api endpoint needs some explanation: @@ -57,7 +59,7 @@ public class UploadController : Controller if (path.Contains("..")) { - Logger.Warn("A path transversal attack has been detected while processing upload path", "security"); + Logger.LogWarning("A path transversal attack has been detected while processing upload path: {path}", path); return BadRequest("Invalid path. This attempt has been logged ;)"); } diff --git a/Moonlight/Features/FileManager/Implementations/ArchiveContextAction.cs b/Moonlight/Features/FileManager/Implementations/ArchiveContextAction.cs index f2045a7e..a9d67057 100644 --- a/Moonlight/Features/FileManager/Implementations/ArchiveContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/ArchiveContextAction.cs @@ -1,6 +1,6 @@ +using MoonCore.Blazor.Services; using MoonCore.Exceptions; using MoonCore.Helpers; -using MoonCoreUI.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; @@ -23,37 +23,39 @@ public class ArchiveContextAction : IFileManagerContextAction var alertService = provider.GetRequiredService(); - var fileName = await alertService.Text("Enter the archive file name", "", + await alertService.Text("Create an archive", "Enter the archive file name", + async fileName => + { + if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled + return; + + var toastService = provider.GetRequiredService(); + + await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient"); + + try + { + await archiveAccess.Archive( + access.CurrentDirectory + fileName, + new[] { access.CurrentDirectory + entry.Name } + ); + + await toastService.Success("Successfully created archive"); + } + catch (Exception e) + { + var logger = provider.GetRequiredService>(); + logger.LogWarning("An error occured while archiving item ({name}): {e}", entry.Name, e); + + await toastService.Danger("An unknown error occured while creating archive"); + } + finally + { + await toastService.DeleteProgress("fileManagerArchive"); + } + + await fileManager.View.Refresh(); + }, Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); - - if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled - return; - - var toastService = provider.GetRequiredService(); - - await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient"); - - try - { - await archiveAccess.Archive( - access.CurrentDirectory + fileName, - new[] { access.CurrentDirectory + entry.Name } - ); - - await toastService.Success("Successfully created archive"); - } - catch (Exception e) - { - Logger.Warn($"An error occured while archiving item ({entry.Name}):"); - Logger.Warn(e); - - await toastService.Danger("An unknown error occured while creating archive"); - } - finally - { - await toastService.RemoveProgress("fileManagerArchive"); - } - - await fileManager.View.Refresh(); } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Implementations/ArchiveSelectionAction.cs b/Moonlight/Features/FileManager/Implementations/ArchiveSelectionAction.cs index 240372e1..ad632a39 100644 --- a/Moonlight/Features/FileManager/Implementations/ArchiveSelectionAction.cs +++ b/Moonlight/Features/FileManager/Implementations/ArchiveSelectionAction.cs @@ -1,6 +1,6 @@ +using MoonCore.Blazor.Services; using MoonCore.Exceptions; using MoonCore.Helpers; -using MoonCoreUI.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; @@ -21,35 +21,36 @@ public class ArchiveSelectionAction : IFileManagerSelectionAction var alertService = provider.GetRequiredService(); - var fileName = await alertService.Text("Enter the archive file name", "", + await alertService.Text("Create an archive", "Enter the archive file name", async fileName => + { + if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled + return; + + var toastService = provider.GetRequiredService(); + + await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient"); + + try + { + await archiveAccess.Archive( + access.CurrentDirectory + fileName, + entries.Select(x => access.CurrentDirectory + x.Name).ToArray() + ); + + await toastService.Success("Successfully created archive"); + } + catch (Exception e) + { + var logger = provider.GetRequiredService>(); + logger.LogWarning("An error occured while archiving items ({lenght}): {e}", entries.Length, e); + + await toastService.Danger("An unknown error occured while creating archive"); + } + finally + { + await toastService.DeleteProgress("fileManagerArchive"); + } + }, Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz"); - - if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled - return; - - var toastService = provider.GetRequiredService(); - - await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient"); - - try - { - await archiveAccess.Archive( - access.CurrentDirectory + fileName, - entries.Select(x => access.CurrentDirectory + x.Name).ToArray() - ); - - await toastService.Success("Successfully created archive"); - } - catch (Exception e) - { - Logger.Warn($"An error occured while archiving items ({entries.Length}):"); - Logger.Warn(e); - - await toastService.Danger("An unknown error occured while creating archive"); - } - finally - { - await toastService.RemoveProgress("fileManagerArchive"); - } } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Implementations/CreateFileAction.cs b/Moonlight/Features/FileManager/Implementations/CreateFileAction.cs index 68589af9..04e25fd0 100644 --- a/Moonlight/Features/FileManager/Implementations/CreateFileAction.cs +++ b/Moonlight/Features/FileManager/Implementations/CreateFileAction.cs @@ -1,4 +1,4 @@ -using MoonCoreUI.Services; +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; @@ -14,21 +14,22 @@ public class CreateFileAction : IFileManagerCreateAction { var alertService = provider.GetRequiredService(); - var name = await alertService.Text("Enter a name for the new file"); - - if (string.IsNullOrEmpty(name) || name.Contains("..")) - return; - - await access.CreateFile(name); - - // We build a virtual entry here so we dont need to fetch one - await fileManager.OpenEditor(new() + await alertService.Text("Create a new file","Enter a name for the new file", async name => { - Name = name, - Size = 0, - IsFile = true, - IsDirectory = false, - LastModifiedAt = DateTime.UtcNow + if (string.IsNullOrEmpty(name) || name.Contains("..")) + return; + + await access.CreateFile(name); + + // We build a virtual entry here so we dont need to fetch one + await fileManager.OpenEditor(new() + { + Name = name, + Size = 0, + IsFile = true, + IsDirectory = false, + LastModifiedAt = DateTime.UtcNow + }); }); } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Implementations/CreateFolderAction.cs b/Moonlight/Features/FileManager/Implementations/CreateFolderAction.cs index 1b8fb36d..2a2c8671 100644 --- a/Moonlight/Features/FileManager/Implementations/CreateFolderAction.cs +++ b/Moonlight/Features/FileManager/Implementations/CreateFolderAction.cs @@ -1,4 +1,4 @@ -using MoonCoreUI.Services; +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.UI.Components; @@ -16,14 +16,15 @@ public class CreateFolderAction : IFileManagerCreateAction var alertService = provider.GetRequiredService(); var toastService = provider.GetRequiredService(); - var name = await alertService.Text("Enter a name for the new directory"); + await alertService.Text("Create a new folder", "Enter a name for the new directory", async name => + { + if (string.IsNullOrEmpty(name) || name.Contains("..")) + return; - if (string.IsNullOrEmpty(name) || name.Contains("..")) - return; + await access.CreateDirectory(name); - await access.CreateDirectory(name); - - await toastService.Success("Successfully created directory"); - await fileManager.View.Refresh(); + await toastService.Success("Successfully created directory"); + await fileManager.View.Refresh(); + }); } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Implementations/DeleteContextAction.cs b/Moonlight/Features/FileManager/Implementations/DeleteContextAction.cs index 223509c6..c721691d 100644 --- a/Moonlight/Features/FileManager/Implementations/DeleteContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/DeleteContextAction.cs @@ -1,4 +1,5 @@ -using MoonCoreUI.Services; + +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.UI.Components; diff --git a/Moonlight/Features/FileManager/Implementations/DeleteSelectionAction.cs b/Moonlight/Features/FileManager/Implementations/DeleteSelectionAction.cs index c63c4e57..b2c1655f 100644 --- a/Moonlight/Features/FileManager/Implementations/DeleteSelectionAction.cs +++ b/Moonlight/Features/FileManager/Implementations/DeleteSelectionAction.cs @@ -1,4 +1,5 @@ -using MoonCoreUI.Services; + +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.UI.Components; @@ -42,23 +43,24 @@ public class DeleteSelectionAction : IFileManagerSelectionAction if (fileCount > showFileCount) fileList += "And " + (fileCount - showFileCount) + " more files..."; - - - if(!await alertService.YesNo($"Do you really want to delete {folderCount + fileCount} item(s)? \n\n" + fileList)) - return; - await toastService.CreateProgress("fileManagerSelectionDelete", "Deleting items"); - foreach (var entry in entries) - { - await toastService.ModifyProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'"); + await alertService.Confirm("Confirm file deletion", + $"Do you really want to delete {folderCount + fileCount} item(s)? \n\n" + fileList, async () => + { + await toastService.CreateProgress("fileManagerSelectionDelete", "Deleting items"); - await access.Delete(entry); - } + foreach (var entry in entries) + { + await toastService.UpdateProgress("fileManagerSelectionDelete", $"Deleting '{entry.Name}'"); - await toastService.RemoveProgress("fileManagerSelectionDelete"); + await access.Delete(entry); + } - await toastService.Success("Successfully deleted selection"); - await fileManager.View.Refresh(); + await toastService.DeleteProgress("fileManagerSelectionDelete"); + + await toastService.Success("Successfully deleted selection"); + await fileManager.View.Refresh(); + }); } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Implementations/DownloadContextAction.cs b/Moonlight/Features/FileManager/Implementations/DownloadContextAction.cs index 6a56beec..e629869c 100644 --- a/Moonlight/Features/FileManager/Implementations/DownloadContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/DownloadContextAction.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; +using MoonCore.Blazor.Services; using MoonCore.Helpers; -using MoonCoreUI.Services; + using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.Services; @@ -15,11 +16,11 @@ public class DownloadContextAction : IFileManagerContextAction public string Color => "primary"; public Func Filter => entry => entry.IsFile; - public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider) + public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider) { - var fileAccessService = serviceProvider.GetRequiredService(); - var navigation = serviceProvider.GetRequiredService(); - var toastService = serviceProvider.GetRequiredService(); + var fileAccessService = provider.GetRequiredService(); + var navigation = provider.GetRequiredService(); + var toastService = provider.GetRequiredService(); try { @@ -31,8 +32,8 @@ public class DownloadContextAction : IFileManagerContextAction } catch (Exception e) { - Logger.Warn("Unable to start download"); - Logger.Warn(e); + var logger = provider.GetRequiredService>(); + logger.LogWarning("Unable to start download: {e}", e); ; await toastService.Danger("Failed to start download"); } diff --git a/Moonlight/Features/FileManager/Implementations/ExtractContextAction.cs b/Moonlight/Features/FileManager/Implementations/ExtractContextAction.cs index 2b8735fe..46ca96a7 100644 --- a/Moonlight/Features/FileManager/Implementations/ExtractContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/ExtractContextAction.cs @@ -1,6 +1,7 @@ +using MoonCore.Blazor.Services; using MoonCore.Exceptions; using MoonCore.Helpers; -using MoonCoreUI.Services; + using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; @@ -36,14 +37,14 @@ public class ExtractContextAction : IFileManagerContextAction } catch (Exception e) { - Logger.Warn($"An error occured while extracting archive ({entry.Name}):"); - Logger.Warn(e); + var logger = provider.GetRequiredService>(); + logger.LogWarning("An error occured while extracting archive ({name}): {e}", entry.Name, e); await toastService.Danger("An unknown error occured while extracting archive"); } finally { - await toastService.RemoveProgress("fileManagerExtract"); + await toastService.DeleteProgress("fileManagerExtract"); } await fileManager.View.Refresh(); diff --git a/Moonlight/Features/FileManager/Implementations/MoveContextAction.cs b/Moonlight/Features/FileManager/Implementations/MoveContextAction.cs index b5f3a7ce..b6f10e1c 100644 --- a/Moonlight/Features/FileManager/Implementations/MoveContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/MoveContextAction.cs @@ -1,4 +1,5 @@ -using MoonCoreUI.Services; + +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; diff --git a/Moonlight/Features/FileManager/Implementations/MoveSelectionAction.cs b/Moonlight/Features/FileManager/Implementations/MoveSelectionAction.cs index 981d9835..a0927959 100644 --- a/Moonlight/Features/FileManager/Implementations/MoveSelectionAction.cs +++ b/Moonlight/Features/FileManager/Implementations/MoveSelectionAction.cs @@ -1,4 +1,5 @@ -using MoonCoreUI.Services; + +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; @@ -19,12 +20,12 @@ public class MoveSelectionAction : IFileManagerSelectionAction foreach (var entry in entries) { - await toastService.ModifyProgress("fileManagerSelectionMove", $"Moving '{entry.Name}'"); + await toastService.UpdateProgress("fileManagerSelectionMove", $"Moving '{entry.Name}'"); await access.Move(entry, path + entry.Name); } - await toastService.RemoveProgress("fileManagerSelectionMove"); + await toastService.DeleteProgress("fileManagerSelectionMove"); await toastService.Success("Successfully moved selection"); await fileManager.View.Refresh(); diff --git a/Moonlight/Features/FileManager/Implementations/RenameContextAction.cs b/Moonlight/Features/FileManager/Implementations/RenameContextAction.cs index 996dffd3..060fa906 100644 --- a/Moonlight/Features/FileManager/Implementations/RenameContextAction.cs +++ b/Moonlight/Features/FileManager/Implementations/RenameContextAction.cs @@ -1,4 +1,5 @@ -using MoonCoreUI.Services; + +using MoonCore.Blazor.Services; using Moonlight.Features.FileManager.Interfaces; using Moonlight.Features.FileManager.Models.Abstractions.FileAccess; using Moonlight.Features.FileManager.UI.Components; @@ -17,14 +18,15 @@ public class RenameContextAction : IFileManagerContextAction var alertService = provider.GetRequiredService(); var toastService = provider.GetRequiredService(); - var newName = await alertService.Text($"Enter a new name for '{entry.Name}'", "", entry.Name); + await alertService.Text("Rename file" , $"Enter a new name for '{entry.Name}'", async newName => + { + if (string.IsNullOrEmpty(newName)) + return; - if (string.IsNullOrEmpty(newName)) - return; + await access.Rename(entry.Name, newName); - await access.Rename(entry.Name, newName); - - await fileManager.View.Refresh(); - await toastService.Success("Successfully renamed file"); + await fileManager.View.Refresh(); + await toastService.Success("Successfully renamed file"); + }, entry.Name); } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Services/FileManagerInteropService.cs b/Moonlight/Features/FileManager/Services/FileManagerInteropService.cs index 208908c2..dbd7bf61 100644 --- a/Moonlight/Features/FileManager/Services/FileManagerInteropService.cs +++ b/Moonlight/Features/FileManager/Services/FileManagerInteropService.cs @@ -1,5 +1,6 @@ using Microsoft.JSInterop; using MoonCore.Attributes; +using MoonCore.Blazor.Extensions; using MoonCore.Helpers; namespace Moonlight.Features.FileManager.Services; @@ -9,32 +10,30 @@ public class FileManagerInteropService { private readonly IJSRuntime JsRuntime; - public SmartEventHandler OnUploadStateChanged { get; set; } = new(); + public SmartEventHandler OnUploadStateChanged { get; set; } - public FileManagerInteropService(IJSRuntime jsRuntime) + public FileManagerInteropService(IJSRuntime jsRuntime, ILogger eventHandlerLogger) { JsRuntime = jsRuntime; + + OnUploadStateChanged = new(eventHandlerLogger); } public async Task InitDropzone(string id, string urlId) { var reference = DotNetObjectReference.Create(this); - await JsRuntime.InvokeVoidAsync("filemanager.dropzone.init", id, urlId, reference); + await JsRuntime.InvokeVoidAsyncHandled("filemanager.dropzone.init", id, urlId, reference); } public async Task InitFileSelect(string id, string urlId) { var reference = DotNetObjectReference.Create(this); - await JsRuntime.InvokeVoidAsync("filemanager.fileselect.init", id, urlId, reference); + await JsRuntime.InvokeVoidAsyncHandled("filemanager.fileselect.init", id, urlId, reference); } public async Task UpdateUrl(string urlId, string url) { - try - { - await JsRuntime.InvokeVoidAsync("filemanager.updateUrl", urlId, url); - } - catch (TaskCanceledException) { /* ignored */ } + await JsRuntime.InvokeVoidAsyncHandled("filemanager.updateUrl", urlId, url); } [JSInvokable] diff --git a/Moonlight/Features/FileManager/UI/Components/FileEditor.razor b/Moonlight/Features/FileManager/UI/Components/FileEditor.razor index c63ed457..f8e6d6f0 100644 --- a/Moonlight/Features/FileManager/UI/Components/FileEditor.razor +++ b/Moonlight/Features/FileManager/UI/Components/FileEditor.razor @@ -1,4 +1,3 @@ -@using MoonCoreUI.Services @using Moonlight.Core.Services @using MoonCore.Helpers @using Moonlight.Core.Helpers @@ -7,6 +6,7 @@ @inject ToastService ToastService @inject HotKeyService HotKeyService +@inject ILogger Logger @implements IDisposable @@ -76,8 +76,7 @@ } catch (Exception e) { - Logger.Warn($"An unhandled error has occured while saving a file using access type {FileAccess.GetType().FullName}"); - Logger.Warn(e); + Logger.LogWarning("An unhandled error has occured while saving a file using access type {name}: {e}", FileAccess.GetType().FullName, e); await ToastService.Danger("An unknown error has occured while saving the file. Please try again later"); return; diff --git a/Moonlight/Features/FileManager/UI/Components/FileManager.razor b/Moonlight/Features/FileManager/UI/Components/FileManager.razor index e51c145d..81576627 100644 --- a/Moonlight/Features/FileManager/UI/Components/FileManager.razor +++ b/Moonlight/Features/FileManager/UI/Components/FileManager.razor @@ -1,6 +1,5 @@ @using MoonCore.Helpers @using MoonCore.Services -@using MoonCoreUI.Services @using Moonlight.Core.Configuration @using Moonlight.Core.Services @using Moonlight.Features.FileManager.Interfaces @@ -9,6 +8,7 @@ @inject AlertService AlertService @inject ToastService ToastService +@inject ModalService ModalService @inject FileManagerInteropService FileManagerInteropService @inject SharedFileAccessService FileAccessService @inject ConfigService ConfigService @@ -111,28 +111,6 @@ else
- - - - - - } @code @@ -151,15 +129,6 @@ else private FileEntry FileToEdit; private bool ShowEditor = false; - // Folder select dialog - private bool FolderSelectIsOpen = false; - private SmartModal FolderSelectModal; - private BaseFileAccess FolderSelectFileAccess; - private string FolderSelectTitle; - private Func FolderSelectResult; - private FileView FolderSelectView; - private Func FolderSelectFilter => entry => entry.IsDirectory; - private Timer? UploadTokenTimer; protected override async Task OnInitializedAsync() @@ -174,8 +143,7 @@ else { if (!firstRender) return; - - + // Setup upload url update timer UploadTokenTimer = new(async _ => { @@ -307,53 +275,17 @@ else } #endregion - - #region Selects - + public async Task OpenFolderSelect(string title, Func onResult) { - if (FolderSelectIsOpen) - await HideFolderSelect(); - - FolderSelectResult = onResult; - FolderSelectTitle = title; - - FolderSelectFileAccess = FileAccess.Clone(); - await FolderSelectFileAccess.SetDirectory("/"); - - await FolderSelectModal.Show(); + await ModalService.Launch(cssClasses: "modal-lg modal-dialog-centered", buildAttributes: parameters => + { + parameters.Add("Title", title); + parameters.Add("OnResult", onResult); + parameters.Add("FileAccess", FileAccess.Clone()); + }); } - - public async Task HideFolderSelect() - { - await FolderSelectModal.Hide(); - FolderSelectIsOpen = false; - FolderSelectFileAccess.Dispose(); - } - - private async Task SubmitFolderSelect() - { - var path = await FolderSelectFileAccess.GetCurrentDirectory(); - - await HideFolderSelect(); - - await FolderSelectResult.Invoke(path); - } - - private async Task NavigateUpFolderSelect() - { - await FolderSelectFileAccess.ChangeDirectory(".."); - await FolderSelectView.Refresh(); - } - - private async Task EntryClickFolderSelect(FileEntry entry) - { - await FolderSelectFileAccess.ChangeDirectory(entry.Name); - await FolderSelectView.Refresh(); - } - - #endregion - + public async void Dispose() { if (UploadTokenTimer != null) diff --git a/Moonlight/Features/FileManager/UI/Components/FolderSelectModal.razor b/Moonlight/Features/FileManager/UI/Components/FolderSelectModal.razor new file mode 100644 index 00000000..b7136185 --- /dev/null +++ b/Moonlight/Features/FileManager/UI/Components/FolderSelectModal.razor @@ -0,0 +1,61 @@ +@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess + +@implements IDisposable + + + + + +@code +{ + [Parameter] public BaseFileAccess FileAccess { get; set; } + [Parameter] public string Title { get; set; } + [Parameter] public Func OnResult { get; set; } + + private FileView View; + private Func Filter => entry => entry.IsDirectory; + + protected override async Task OnInitializedAsync() + { + await FileAccess.SetDirectory("/"); + } + + private async Task SubmitFolderSelect() + { + var path = await FileAccess.GetCurrentDirectory(); + await OnResult.Invoke(path); + } + + private async Task NavigateUpFolderSelect() + { + await FileAccess.ChangeDirectory(".."); + await View.Refresh(); + } + + private async Task EntryClickFolderSelect(FileEntry entry) + { + await FileAccess.ChangeDirectory(entry.Name); + await View.Refresh(); + } + + public void Dispose() + { + FileAccess.Dispose(); + } +} diff --git a/Moonlight/Features/Servers/Entities/Server.cs b/Moonlight/Features/Servers/Entities/Server.cs index 7377987a..b25d0d94 100644 --- a/Moonlight/Features/Servers/Entities/Server.cs +++ b/Moonlight/Features/Servers/Entities/Server.cs @@ -14,14 +14,14 @@ public class Server public string? OverrideStartupCommand { get; set; } - public int Cpu { get; set; } + public int Cpu { get; set; } = 100; public int Memory { get; set; } public int Disk { get; set; } - public bool UseVirtualDisk { get; set; } + public bool UseVirtualDisk { get; set; } = false; public ServerNode Node { get; set; } public ServerNetwork? Network { get; set; } - public bool DisablePublicNetwork { get; set; } + public bool DisablePublicNetwork { get; set; } = false; public ServerAllocation MainAllocation { get; set; } public List Allocations { get; set; } = new(); diff --git a/Moonlight/Features/Servers/Entities/ServerAllocation.cs b/Moonlight/Features/Servers/Entities/ServerAllocation.cs index 1ba9dfac..de017f3a 100644 --- a/Moonlight/Features/Servers/Entities/ServerAllocation.cs +++ b/Moonlight/Features/Servers/Entities/ServerAllocation.cs @@ -3,7 +3,7 @@ namespace Moonlight.Features.Servers.Entities; public class ServerAllocation { public int Id { get; set; } - public string IpAddress { get; set; } = ""; + public string IpAddress { get; set; } = "0.0.0.0"; public int Port { get; set; } public string Note { get; set; } = ""; } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/ServerDockerImage.cs b/Moonlight/Features/Servers/Entities/ServerDockerImage.cs index 58ce8c4d..9a86c3fe 100644 --- a/Moonlight/Features/Servers/Entities/ServerDockerImage.cs +++ b/Moonlight/Features/Servers/Entities/ServerDockerImage.cs @@ -7,5 +7,5 @@ public class ServerDockerImage public string DisplayName { get; set; } = ""; public string Name { get; set; } = ""; - public bool AutoPull { get; set; } + public bool AutoPull { get; set; } = true; } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Entities/ServerImage.cs b/Moonlight/Features/Servers/Entities/ServerImage.cs index 41e15315..41d215fc 100644 --- a/Moonlight/Features/Servers/Entities/ServerImage.cs +++ b/Moonlight/Features/Servers/Entities/ServerImage.cs @@ -10,13 +10,13 @@ public class ServerImage public string? UpdateUrl { get; set; } public string? DonateUrl { get; set; } - public string StartupCommand { get; set; } = ""; - public string OnlineDetection { get; set; } = ""; - public string StopCommand { get; set; } = ""; + public string StartupCommand { get; set; } = "echo Startup command here"; + public string OnlineDetection { get; set; } = "Running"; + public string StopCommand { get; set; } = "^C"; - public string InstallShell { get; set; } = ""; - public string InstallDockerImage { get; set; } = ""; - public string InstallScript { get; set; } = ""; + public string InstallShell { get; set; } = "/bin/bash"; + public string InstallDockerImage { get; set; } = "debian:latest"; + public string InstallScript { get; set; } = "#! /bin/bash\necho Done"; public string ParseConfiguration { get; set; } = "[]"; public int AllocationsNeeded { get; set; } = 1; diff --git a/Moonlight/Features/Servers/Events/ServerEvents.cs b/Moonlight/Features/Servers/Events/ServerEvents.cs index 78111cdb..0be18882 100644 --- a/Moonlight/Features/Servers/Events/ServerEvents.cs +++ b/Moonlight/Features/Servers/Events/ServerEvents.cs @@ -1,9 +1,16 @@ -using MoonCore.Helpers; +using MoonCore.Attributes; +using MoonCore.Helpers; using Moonlight.Features.Servers.Entities; namespace Moonlight.Features.Servers.Events; +[Singleton] public class ServerEvents { - public static SmartEventHandler<(Server, ServerBackup)> OnBackupCompleted { get; set; } = new(); + public SmartEventHandler<(Server, ServerBackup)> OnBackupCompleted { get; set; } + + public ServerEvents(ILogger eventHandlerLogger) + { + OnBackupCompleted = new(eventHandlerLogger); + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Extensions/NodeExtensions.cs b/Moonlight/Features/Servers/Extensions/NodeExtensions.cs index 436e75e2..320caea5 100644 --- a/Moonlight/Features/Servers/Extensions/NodeExtensions.cs +++ b/Moonlight/Features/Servers/Extensions/NodeExtensions.cs @@ -16,8 +16,13 @@ public static class NodeExtensions return new HttpApiClient(remoteUrl, node.Token); } - public static JwtService CreateJwtService(this ServerNode node) + public static JwtService CreateJwtService(this ServerNode node, ILoggerFactory factory) { - return new JwtService(node.Token); + return node.CreateJwtService(factory.CreateLogger>()); + } + + public static JwtService CreateJwtService(this ServerNode node, ILogger> logger) + { + return new JwtService(node.Token, logger); } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Helpers/ServerConsole.cs b/Moonlight/Features/Servers/Helpers/ServerConsole.cs index 32d0e4ac..23d535db 100644 --- a/Moonlight/Features/Servers/Helpers/ServerConsole.cs +++ b/Moonlight/Features/Servers/Helpers/ServerConsole.cs @@ -8,10 +8,10 @@ namespace Moonlight.Features.Servers.Helpers; public class ServerConsole : IDisposable { - public SmartEventHandler OnStateChange { get; set; } = new(); - public SmartEventHandler OnStatsChange { get; set; } = new(); - public SmartEventHandler OnNewMessage { get; set; } = new(); - public SmartEventHandler OnDisconnected { get; set; } = new(); + public SmartEventHandler OnStateChange { get; set; } + public SmartEventHandler OnStatsChange { get; set; } + public SmartEventHandler OnNewMessage { get; set; } + public SmartEventHandler OnDisconnected { get; set; } public ServerState State { get; private set; } = ServerState.Offline; public ServerStats Stats { get; private set; } = new(); @@ -20,18 +20,29 @@ public class ServerConsole : IDisposable private readonly List MessageCache = new(); private readonly Server Server; + private readonly ILogger Logger; + private readonly ILogger AwsLogger; private ClientWebSocket WebSocket; private AdvancedWebsocketStream WebsocketStream; private CancellationTokenSource Cancellation = new(); - public ServerConsole(Server server) + public ServerConsole(Server server, ILoggerFactory loggerFactory) { if (server.Node == null) throw new ArgumentNullException(nameof(server.Node)); Server = server; + + Logger = loggerFactory.CreateLogger(); + AwsLogger = loggerFactory.CreateLogger(); + + var eventHandlerLogger = loggerFactory.CreateLogger(); + OnStateChange = new(eventHandlerLogger); + OnStatsChange = new(eventHandlerLogger); + OnDisconnected = new(eventHandlerLogger); + OnNewMessage = new(eventHandlerLogger); } public async Task Connect() @@ -49,7 +60,7 @@ public class ServerConsole : IDisposable wsUrl = $"ws://{Server.Node.Fqdn}:{Server.Node.HttpPort}/servers/{Server.Id}/ws"; await WebSocket.ConnectAsync(new Uri(wsUrl), CancellationToken.None); - WebsocketStream = new AdvancedWebsocketStream(WebSocket); + WebsocketStream = new AdvancedWebsocketStream(AwsLogger, WebSocket); WebsocketStream.RegisterPacket(1); WebsocketStream.RegisterPacket(2); @@ -103,11 +114,10 @@ public class ServerConsole : IDisposable break; if (e is WebSocketException) - Logger.Warn($"Lost connection to daemon server websocket: {e.Message}"); + Logger.LogWarning("Lost connection to daemon server websocket: {message}", e.Message); else { - Logger.Warn("Server console ws disconnected because of application error:"); - Logger.Warn(e); + Logger.LogWarning("Server console ws disconnected because of application error: {e}", e); } break; diff --git a/Moonlight/Features/Servers/Http/Controllers/ServersController.cs b/Moonlight/Features/Servers/Http/Controllers/ServersController.cs index ac7c7b9b..4ed3703d 100644 --- a/Moonlight/Features/Servers/Http/Controllers/ServersController.cs +++ b/Moonlight/Features/Servers/Http/Controllers/ServersController.cs @@ -18,11 +18,17 @@ public class ServersController : Controller { private readonly Repository ServerRepository; private readonly Repository BackupRepository; + private readonly ILogger Logger; + private readonly ILogger WebSocketLogger; + private readonly ServerEvents ServerEvents; - public ServersController(Repository serverRepository, Repository backupRepository) + public ServersController(Repository serverRepository, Repository backupRepository, ILogger logger, ILogger webSocketLogger, ServerEvents serverEvents) { ServerRepository = serverRepository; BackupRepository = backupRepository; + Logger = logger; + WebSocketLogger = webSocketLogger; + ServerEvents = serverEvents; } [HttpGet("ws")] @@ -36,7 +42,7 @@ public class ServersController : Controller var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); // Build connection wrapper - var websocketStream = new AdvancedWebsocketStream(websocket); + var websocketStream = new AdvancedWebsocketStream(WebSocketLogger, websocket); websocketStream.RegisterPacket(1); websocketStream.RegisterPacket(2); @@ -66,8 +72,7 @@ public class ServersController : Controller } catch (Exception e) { - Logger.Error($"An error occured while sending server {server.Id} (Image: {server.Image.Name}) to daemon. This may indicate a corrupt or broken image/server. Skipping this server"); - Logger.Error(e); + Logger.LogError("An error occured while sending server {serverId} (Image: {name}) to daemon. This may indicate a corrupt or broken image/server. Skipping this server. Error: {e}", server.Id, server.Image.Name, e); } } @@ -146,7 +151,7 @@ public class ServersController : Controller return NotFound(); if(!status.Successful) - Logger.Warn($"A node reported an error for a backup for the server {server.Id}"); + Logger.LogWarning("A node reported an error for a backup for the server {serverId}", server.Id); backup.Successful = status.Successful; backup.Completed = true; diff --git a/Moonlight/Features/Servers/Implementations/UI/Admin/AdminColumns/ServerCount.cs b/Moonlight/Features/Servers/Implementations/AdminDashboard/Columns/ServerCount.cs similarity index 80% rename from Moonlight/Features/Servers/Implementations/UI/Admin/AdminColumns/ServerCount.cs rename to Moonlight/Features/Servers/Implementations/AdminDashboard/Columns/ServerCount.cs index 3e7928ae..0a2c6f96 100644 --- a/Moonlight/Features/Servers/Implementations/UI/Admin/AdminColumns/ServerCount.cs +++ b/Moonlight/Features/Servers/Implementations/AdminDashboard/Columns/ServerCount.cs @@ -1,9 +1,9 @@ -using MoonCoreUI.Helpers; +using MoonCore.Blazor.Helpers; using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Models.Abstractions; using Moonlight.Features.Servers.UI.Components.Cards; -namespace Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns; +namespace Moonlight.Features.Servers.Implementations.AdminDashboard.Columns; public class ServerCount : IAdminDashboardColumn { diff --git a/Moonlight/Features/Servers/Implementations/UI/Admin/AdminComponents/NodeOverview.cs b/Moonlight/Features/Servers/Implementations/AdminDashboard/Components/NodeOverview.cs similarity index 79% rename from Moonlight/Features/Servers/Implementations/UI/Admin/AdminComponents/NodeOverview.cs rename to Moonlight/Features/Servers/Implementations/AdminDashboard/Components/NodeOverview.cs index cd5116c8..aade955b 100644 --- a/Moonlight/Features/Servers/Implementations/UI/Admin/AdminComponents/NodeOverview.cs +++ b/Moonlight/Features/Servers/Implementations/AdminDashboard/Components/NodeOverview.cs @@ -1,9 +1,9 @@ -using MoonCoreUI.Helpers; +using MoonCore.Blazor.Helpers; using Moonlight.Core.Interfaces.Ui.Admin; using Moonlight.Core.Models.Abstractions; using Moonlight.Features.Servers.UI.Components.Cards; -namespace Moonlight.Features.Servers.Implementations.UI.Admin.AdminComponents; +namespace Moonlight.Features.Servers.Implementations.AdminDashboard.Components; public class NodeOverview : IAdminDashboardComponent { diff --git a/Moonlight/Features/Servers/Implementations/UI/UserDashboard/Components/UserDashboardServerCount.cs b/Moonlight/Features/Servers/Implementations/UserDashboard/Components/UserDashboardServerCount.cs similarity index 72% rename from Moonlight/Features/Servers/Implementations/UI/UserDashboard/Components/UserDashboardServerCount.cs rename to Moonlight/Features/Servers/Implementations/UserDashboard/Components/UserDashboardServerCount.cs index 5a10fee5..fdc2e0ec 100644 --- a/Moonlight/Features/Servers/Implementations/UI/UserDashboard/Components/UserDashboardServerCount.cs +++ b/Moonlight/Features/Servers/Implementations/UserDashboard/Components/UserDashboardServerCount.cs @@ -1,9 +1,8 @@ -using MoonCoreUI.Helpers; +using MoonCore.Blazor.Helpers; using Moonlight.Core.Interfaces.UI.User; using Moonlight.Core.Models.Abstractions; -using Moonlight.Features.Servers.UI.Components.Cards; -namespace Moonlight.Features.Servers.Implementations.UI.UserDashboard.Components; +namespace Moonlight.Features.Servers.Implementations.UserDashboard.Components; public class UserDashboardServerCount : IUserDashboardComponent { diff --git a/Moonlight/Features/Servers/Models/Forms/Admin/Servers/CreateServerForm.cs b/Moonlight/Features/Servers/Models/Forms/Admin/Servers/CreateServerForm.cs index c3f6be26..ae34d1bd 100644 --- a/Moonlight/Features/Servers/Models/Forms/Admin/Servers/CreateServerForm.cs +++ b/Moonlight/Features/Servers/Models/Forms/Admin/Servers/CreateServerForm.cs @@ -1,6 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using MoonCoreUI.Attributes; +using MoonCore.Blazor.Attributes.Auto; using Moonlight.Core.Database.Entities; using Moonlight.Features.Servers.Entities; @@ -12,11 +12,11 @@ public class CreateServerForm public string Name { get; set; } [Required(ErrorMessage = "You need to specify a server owner")] - [Selector(SelectorProp = "Username", DisplayProp = "Username", UseDropdown = true)] + //[Selector(SelectorProp = "Username", DisplayProp = "Username", UseDropdown = true)] public User Owner { get; set; } [Required(ErrorMessage = "You need to specify a server image")] - [Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] + //[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] public ServerImage Image { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Enter a valid cpu value")] @@ -26,31 +26,31 @@ public class CreateServerForm [Range(1, int.MaxValue, ErrorMessage = "Enter a valid memory value")] [Description("The amount of memory this server will be able to use")] - [ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] + //[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] [Section("Resources", Icon = "bxs-chip")] public int Memory { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Enter a valid disk value")] [Description("The amount of disk space this server will be able to use")] - [ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] + //[ByteSize(MinimumUnit = 1, Converter = 1, DefaultUnit = 2)] [Section("Resources", Icon = "bxs-chip")] public int Disk { get; set; } [Description("Whether to use a virtual disk for storing server files. Dont use this if you want to overallocate as the virtual disks will fill out the space you allocate")] [Section("Deployment", Icon = "bx-cube")] - [RadioButtonBool("Virtual Disk", "Simple Volume", TrueIcon = "bxs-hdd", FalseIcon = "bxs-data")] + //[RadioButtonBool("Virtual Disk", "Simple Volume", TrueIcon = "bxs-hdd", FalseIcon = "bxs-data")] [DisplayName("Storage")] public bool UseVirtualDisk { get; set; } [Required(ErrorMessage = "You need to specify a server node")] - [Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] + //[Selector(SelectorProp = "Name", DisplayProp = "Name", UseDropdown = true)] [Section("Deployment", Icon = "bx-cube")] public ServerNode Node { get; set; } [Description("The allocations the server should have")] - [MultiSelection("Port", "Port", Icon = "bx-network-chart")] + //TODO: [MultiSelection("Port", "Port", Icon = "bx-network-chart")] [Section("Deployment", Icon = "bx-cube")] - [CustomItemLoader("FreeAllocations")] - [CustomDisplayFunction("AllocationWithIp")] + //[CustomItemLoader("FreeAllocations")] + //[CustomDisplayFunction("AllocationWithIp")] public List Allocations { get; set; } = new(); } \ No newline at end of file diff --git a/Moonlight/Features/Servers/Models/Forms/Users/Networks/CreateNetworkForm.cs b/Moonlight/Features/Servers/Models/Forms/Users/Networks/CreateNetworkForm.cs index 6c9cce05..299b4a4f 100644 --- a/Moonlight/Features/Servers/Models/Forms/Users/Networks/CreateNetworkForm.cs +++ b/Moonlight/Features/Servers/Models/Forms/Users/Networks/CreateNetworkForm.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using MoonCoreUI.Attributes; using Moonlight.Features.Servers.Entities; namespace Moonlight.Features.Servers.Models.Forms.Users.Networks; @@ -10,6 +9,6 @@ public class CreateNetworkForm public string Name { get; set; } [Required(ErrorMessage = "You need to specify a node to create the network on")] - [Selector(SelectorProp = "Name", DisplayProp = "Name")] + //[Selector(SelectorProp = "Name", DisplayProp = "Name")] public ServerNode Node { get; set; } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/ServersFeature.cs b/Moonlight/Features/Servers/ServersFeature.cs index 8ebf44f7..1e1ee4d1 100644 --- a/Moonlight/Features/Servers/ServersFeature.cs +++ b/Moonlight/Features/Servers/ServersFeature.cs @@ -9,13 +9,13 @@ using Moonlight.Core.Services; using Moonlight.Features.Servers.Actions; using Moonlight.Features.Servers.Configuration; using Moonlight.Features.Servers.Http.Middleware; +using Moonlight.Features.Servers.Implementations.AdminDashboard.Columns; +using Moonlight.Features.Servers.Implementations.AdminDashboard.Components; using Moonlight.Features.Servers.Implementations.Diagnose; -using Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns; -using Moonlight.Features.Servers.Implementations.UI.Admin.AdminComponents; using Moonlight.Features.Servers.Models.Enums; using Moonlight.Features.Servers.Services; using Moonlight.Features.Servers.UI.Components.Cards; -using UserDashboardServerCount = Moonlight.Features.Servers.Implementations.UI.UserDashboard.Components.UserDashboardServerCount; +using UserDashboardServerCount = Moonlight.Features.Servers.Implementations.UserDashboard.Components.UserDashboardServerCount; namespace Moonlight.Features.Servers; @@ -34,7 +34,7 @@ public class ServersFeature : MoonlightFeature // var config = new ConfigService(PathBuilder.File("storage", "configs", "core.json")); - context.Builder.Services.AddSingleton(new JwtService(config.Get().Security.Token)); + context.Builder.Services.AddSingleton(new JwtService(config.Get().Security.Token, context.LoggerFactory.CreateLogger>())); // var configService = new ConfigService(PathBuilder.File("storage", "configs", "servers.json")); diff --git a/Moonlight/Features/Servers/Services/NodeService.cs b/Moonlight/Features/Servers/Services/NodeService.cs index df39ed75..f5522462 100644 --- a/Moonlight/Features/Servers/Services/NodeService.cs +++ b/Moonlight/Features/Servers/Services/NodeService.cs @@ -11,10 +11,12 @@ namespace Moonlight.Features.Servers.Services; public class NodeService { private readonly IServiceProvider ServiceProvider; + private readonly ILogger Logger; - public NodeService(IServiceProvider serviceProvider) + public NodeService(IServiceProvider serviceProvider, ILogger logger) { ServiceProvider = serviceProvider; + Logger = logger; } public async Task Boot(ServerNode node) @@ -42,8 +44,7 @@ public class NodeService { //TODO: Add http exception check to reduce error logs - Logger.Warn($"An error occured while booting node '{node.Name}'"); - Logger.Warn(e); + Logger.LogWarning("An error occured while booting node '{name}': {e}", node.Name, e); } }); } diff --git a/Moonlight/Features/Servers/Services/ServerBackupService.cs b/Moonlight/Features/Servers/Services/ServerBackupService.cs index 84190a75..f5aaa995 100644 --- a/Moonlight/Features/Servers/Services/ServerBackupService.cs +++ b/Moonlight/Features/Servers/Services/ServerBackupService.cs @@ -116,7 +116,8 @@ public class ServerBackupService var remoteUrl = $"{protocol}://{node.Fqdn}:{node.HttpPort}/"; // Build jwt - var jwtService = node.CreateJwtService(); + var loggerFactory = ServiceProvider.GetRequiredService(); + var jwtService = node.CreateJwtService(loggerFactory); var jwt = await jwtService.Create(data => { diff --git a/Moonlight/Features/Servers/Services/ServerScheduleService.cs b/Moonlight/Features/Servers/Services/ServerScheduleService.cs index b82a4ac6..85e501de 100644 --- a/Moonlight/Features/Servers/Services/ServerScheduleService.cs +++ b/Moonlight/Features/Servers/Services/ServerScheduleService.cs @@ -14,10 +14,12 @@ public class ServerScheduleService { private readonly IServiceProvider ServiceProvider; public readonly Dictionary Actions = new(); + private readonly ILogger Logger; - public ServerScheduleService(IServiceProvider serviceProvider) + public ServerScheduleService(IServiceProvider serviceProvider, ILogger logger) { ServiceProvider = serviceProvider; + Logger = logger; } public Task RegisterAction(string id) where T : ScheduleAction @@ -50,7 +52,7 @@ public class ServerScheduleService { if (!Actions.ContainsKey(scheduleItem.Action)) { - Logger.Warn($"The server {server.Id} has a invalid action type '{scheduleItem.Action}'"); + Logger.LogWarning("The server {serverId} has a invalid action type '{action}'", server.Id, scheduleItem.Action); continue; } @@ -69,8 +71,7 @@ public class ServerScheduleService } catch (Exception e) { - Logger.Warn($"An unhandled error occured while running schedule {schedule.Name} for server {server.Id}"); - Logger.Warn(e); + Logger.LogWarning("An unhandled error occured while running schedule {name} for server {serverId}: {e}", schedule.Name, server.Id, e); sw.Stop(); diff --git a/Moonlight/Features/Servers/Services/ServerService.cs b/Moonlight/Features/Servers/Services/ServerService.cs index 219515ee..2fb857de 100644 --- a/Moonlight/Features/Servers/Services/ServerService.cs +++ b/Moonlight/Features/Servers/Services/ServerService.cs @@ -27,10 +27,12 @@ public class ServerService public NodeService NodeService => ServiceProvider.GetRequiredService(); private readonly IServiceProvider ServiceProvider; + private readonly ILogger Logger; - public ServerService(IServiceProvider serviceProvider) + public ServerService(IServiceProvider serviceProvider, ILogger logger) { ServiceProvider = serviceProvider; + Logger = logger; } public async Task Sync(Server server) @@ -84,8 +86,7 @@ public class ServerService } catch (Exception e) { - Logger.Warn($"Could not establish to the node with the id {node.Id}"); - Logger.Warn(e); + Logger.LogWarning("Could not establish to the node with the id {nodeId}: {e}", node.Id, e); throw new DisplayException($"Could not establish connection to the node: {e.Message}"); } diff --git a/Moonlight/Features/Servers/UI/Components/Cards/UserDashboardServerCount.razor b/Moonlight/Features/Servers/UI/Components/Cards/UserDashboardServerCount.razor index d5bb61c0..e77fa8a4 100644 --- a/Moonlight/Features/Servers/UI/Components/Cards/UserDashboardServerCount.razor +++ b/Moonlight/Features/Servers/UI/Components/Cards/UserDashboardServerCount.razor @@ -32,8 +32,8 @@ { if (firstRender) { - ServerCount = await ServerRepository.Get().Where(x => x.Owner == IdentityService.CurrentUser).CountAsync(); - NetworksCount = await NetworkRepository.Get().Where(x => x.User == IdentityService.CurrentUser).CountAsync(); + ServerCount = await ServerRepository.Get().Where(x => x.Owner == IdentityService.GetUser()).CountAsync(); + NetworksCount = await NetworkRepository.Get().Where(x => x.User == IdentityService.GetUser()).CountAsync(); await InvokeAsync(StateHasChanged); } } diff --git a/Moonlight/Features/Servers/UI/Components/Terminal.razor b/Moonlight/Features/Servers/UI/Components/Terminal.razor index 19a5ca32..3e5911e9 100644 --- a/Moonlight/Features/Servers/UI/Components/Terminal.razor +++ b/Moonlight/Features/Servers/UI/Components/Terminal.razor @@ -1,6 +1,6 @@ @using XtermBlazor -@using MoonCoreUI.Services + @inject ClipboardService ClipboardService @inject ToastService ToastService diff --git a/Moonlight/Features/Servers/UI/Components/VariableViews/NumberVariableView.razor b/Moonlight/Features/Servers/UI/Components/VariableViews/NumberVariableView.razor index 83854b5e..ab76c7ed 100644 --- a/Moonlight/Features/Servers/UI/Components/VariableViews/NumberVariableView.razor +++ b/Moonlight/Features/Servers/UI/Components/VariableViews/NumberVariableView.razor @@ -1,6 +1,5 @@ @using Moonlight.Features.Servers.Entities @using MoonCore.Abstractions -@using MoonCoreUI.Services @inject Repository ServerVariableRepository @inject ToastService ToastService diff --git a/Moonlight/Features/Servers/UI/Components/VariableViews/SelectVariableView.razor b/Moonlight/Features/Servers/UI/Components/VariableViews/SelectVariableView.razor index bf53dc01..8c0be787 100644 --- a/Moonlight/Features/Servers/UI/Components/VariableViews/SelectVariableView.razor +++ b/Moonlight/Features/Servers/UI/Components/VariableViews/SelectVariableView.razor @@ -1,6 +1,6 @@ @using Moonlight.Features.Servers.Entities @using MoonCore.Abstractions -@using MoonCoreUI.Services + @using System.Text.RegularExpressions @using MoonCore.Helpers diff --git a/Moonlight/Features/Servers/UI/Components/VariableViews/TextVariableView.razor b/Moonlight/Features/Servers/UI/Components/VariableViews/TextVariableView.razor index e0d11804..0cbae421 100644 --- a/Moonlight/Features/Servers/UI/Components/VariableViews/TextVariableView.razor +++ b/Moonlight/Features/Servers/UI/Components/VariableViews/TextVariableView.razor @@ -1,6 +1,6 @@ @using Moonlight.Features.Servers.Entities @using MoonCore.Abstractions -@using MoonCoreUI.Services + @using System.Text.RegularExpressions @inject Repository ServerVariableRepository diff --git a/Moonlight/Features/Servers/UI/Components/VariableViews/ToggleVariableView.razor b/Moonlight/Features/Servers/UI/Components/VariableViews/ToggleVariableView.razor index 1a613b0c..4adfac33 100644 --- a/Moonlight/Features/Servers/UI/Components/VariableViews/ToggleVariableView.razor +++ b/Moonlight/Features/Servers/UI/Components/VariableViews/ToggleVariableView.razor @@ -1,6 +1,6 @@ @using Moonlight.Features.Servers.Entities @using MoonCore.Abstractions -@using MoonCoreUI.Services + @inject Repository ServerVariableRepository @inject ToastService ToastService diff --git a/Moonlight/Features/Servers/UI/ImageComponents/DefaultDockerImage.razor b/Moonlight/Features/Servers/UI/ImageComponents/DefaultDockerImage.razor new file mode 100644 index 00000000..0269c82b --- /dev/null +++ b/Moonlight/Features/Servers/UI/ImageComponents/DefaultDockerImage.razor @@ -0,0 +1,51 @@ +@using Microsoft.CSharp.RuntimeBinder +@using Moonlight.Features.Servers.Entities + +@inherits FastFormBaseComponent + +
+ + + +
+ +@code +{ + [Parameter] public ServerImage Image { get; set; } + + private ServerDockerImage? SelectedDockerImage + { + get + { + if (Binder.Value >= SortedImages.Count) + return null; + + if (Binder.Value == -1) + return null; + + return SortedImages[Binder.Value]; + } + set + { + if (value == null) + { + Binder.Value = -1; + return; + } + + Binder.Value = SortedImages.IndexOf(value); + } + } + + private List SortedImages; + + protected override void OnInitialized() + { + SortedImages = Image.DockerImages + .OrderBy(x => x.Id) + .ToList(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/ImageComponents/EditorComponent.razor b/Moonlight/Features/Servers/UI/ImageComponents/EditorComponent.razor new file mode 100644 index 00000000..c7cfe9c0 --- /dev/null +++ b/Moonlight/Features/Servers/UI/ImageComponents/EditorComponent.razor @@ -0,0 +1,28 @@ +@using Moonlight.Features.FileManager.UI.Components + +@inherits FastFormBaseComponent + +
+ + + +
+ +@code +{ + [Parameter] public string Mode { get; set; } = "sh"; + + [Parameter] public int Lines { get; set; } = 25; + + private Task OnValueChanged(string val) + { + Binder.Value = val; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/ImageComponents/ImageDetails.razor b/Moonlight/Features/Servers/UI/ImageComponents/ImageDetails.razor index d19a4678..2c531345 100644 --- a/Moonlight/Features/Servers/UI/ImageComponents/ImageDetails.razor +++ b/Moonlight/Features/Servers/UI/ImageComponents/ImageDetails.razor @@ -1,8 +1,9 @@ @using Moonlight.Features.Servers.Models.Forms.Admin.Images
-
+
@*
+ @@ -21,7 +22,7 @@ -
+
*@
diff --git a/Moonlight/Features/Servers/UI/ImageComponents/ImageDockerImages.razor b/Moonlight/Features/Servers/UI/ImageComponents/ImageDockerImages.razor index 6b131e24..ac891219 100644 --- a/Moonlight/Features/Servers/UI/ImageComponents/ImageDockerImages.razor +++ b/Moonlight/Features/Servers/UI/ImageComponents/ImageDockerImages.razor @@ -1,85 +1,102 @@ -@using Moonlight.Features.Servers.Models.Forms.Admin.Images.DockerImages -@using Microsoft.AspNetCore.Components.Forms +@using MoonCore.Abstractions +@using MoonCore.Exceptions @using Moonlight.Features.Servers.Entities -@using Moonlight.Features.Servers.Models.Forms.Admin.Images -
-
-
-
- - -
-
- -
- -
-
-
-
-
+@inject Repository ImageRepository +@inject Repository DockerImageRepository - + - - - - + + + + - + - - - Add a new docker image in order to get started. Need help? Check out our documentation - - - + @code { [Parameter] public ServerImage Image { get; set; } - [Parameter] public UpdateImageDetailedForm Form { get; set; } - - private ServerDockerImage? SelectedDockerImage + + private IEnumerable Loader(Repository repository) { - get - { - if (Form.DefaultDockerImage >= Image.DockerImages.Count) - return null; + return Image.DockerImages; + } - if (Form.DefaultDockerImage == -1) - return null; - - return Image.DockerImages[Form.DefaultDockerImage]; - } - set + private void OnConfigure(FastCrudConfiguration configuration) + { + configuration.CustomCreate = dockerImage => { - if (value == null) + Image.DockerImages.Add(dockerImage); + ImageRepository.Update(Image); + + return Task.CompletedTask; + }; + + configuration.CustomDelete = dockerImage => + { + Image.DockerImages.Remove(dockerImage); + ImageRepository.Update(Image); + + try { - Form.DefaultDockerImage = -1; - return; + DockerImageRepository.Delete(dockerImage); } + catch (Exception) + { + /* Dont fail here */ + } + + return Task.CompletedTask; + }; - Form.DefaultDockerImage = Image.DockerImages.IndexOf(value); - } + configuration.ValidateCreate = dockerImage => + { + if (Image.DockerImages.Any(x => x.Name == dockerImage.Name)) + throw new DisplayException("A docker image with this name does already exist"); + + return Task.CompletedTask; + }; + + configuration.ValidateEdit = dockerImage => + { + if (Image.DockerImages.Any(x => x.Name == dockerImage.Name && x.Id != dockerImage.Id)) + throw new DisplayException("A docker image with this name does already exist"); + + return Task.CompletedTask; + }; + } + + private void OnConfigureForm(FastFormConfiguration configuration, ServerDockerImage _) + { + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^(?:[a-zA-Z0-9\\-\\.]+\\/)?[a-zA-Z0-9\\-]+(?:\\/[a-zA-Z0-9\\-]+)*(?::[a-zA-Z0-9_\\.-]+)?$", "You need to provide a valid docker image name")) + .WithDescription("This is the name of the docker image. E.g. moonlightpanel/moonlight:canary"); + + configuration.AddProperty(x => x.DisplayName) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithDescription("This will be shown if the user is able to change the docker image as the image name"); + + configuration.AddProperty(x => x.AutoPull) + .WithComponent() + .WithDescription("Specifies if the docker image should be pulled/updated when creating a server instance. Disable this for only local existing docker images"); } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/ImageComponents/ImageInstall.razor b/Moonlight/Features/Servers/UI/ImageComponents/ImageInstall.razor index 6c2baefc..ea080729 100644 --- a/Moonlight/Features/Servers/UI/ImageComponents/ImageInstall.razor +++ b/Moonlight/Features/Servers/UI/ImageComponents/ImageInstall.razor @@ -2,7 +2,7 @@ @using Moonlight.Features.FileManager.UI.Components
-
+
@*
-
+
*@
+ +
+ +
+ +@foreach (var config in Configs) +{ +
+
+
+ +
+
+
+
+
+ +
+ A relative path from the servers main directory to the file you want to modify +
+ +
+
+ +
+ This specifies the type of parser to use. e.g. "properties" or "file" +
+ +
+
+
+ Remove +
+
+
+
+
+
+
+ +
+
+
+
+ @foreach (var option in config.Value) + { +
+
+ + + Remove +
+
+ } +
+
+
+
+
+
+
+} + +@code +{ + [Parameter] public string InitialContent { get; set; } + + private Dictionary> Configs = new(); + + protected override async Task OnInitializedAsync() + { + await Set(Binder.Value); + } + + private async Task RefreshProperty() + { + Binder.Value = await Get(); + } + + public async Task Set(string content) + { + Configs.Clear(); + + var configs = JsonConvert.DeserializeObject(content) ?? []; + + foreach (var config in configs) + { + var options = config.Configuration.Select(x => new ParseConfigOptionForm() + { + Key = x.Key, + Value = x.Value + }).ToList(); + + + Configs.Add(Mapper.Map(config), options); + } + + await InvokeAsync(StateHasChanged); + } + + public Task Get() + { + var finalConfigs = Configs.Select(x => new ServerParseConfig() + { + File = x.Key.File, + Type = x.Key.Type, + Configuration = x.Value.ToDictionary(y => y.Key, y => y.Value) + }).ToList(); + + var result = JsonConvert.SerializeObject(finalConfigs); + + return Task.FromResult(result); + } + + private async Task AddConfig() + { + Configs.Add(new(), new()); + await InvokeAsync(StateHasChanged); + + await RefreshProperty(); + } + + private async Task RemoveConfig(ParseConfigForm config) + { + Configs.Remove(config); + await InvokeAsync(StateHasChanged); + + await RefreshProperty(); + } + + private async Task AddOption(ParseConfigForm config) + { + Configs[config].Add(new()); + await InvokeAsync(StateHasChanged); + + await RefreshProperty(); + } + + private async Task RemoveOption(ParseConfigForm config, ParseConfigOptionForm option) + { + Configs[config].Remove(option); + await InvokeAsync(StateHasChanged); + + await RefreshProperty(); + } +} \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/ImageComponents/ImagePower.razor b/Moonlight/Features/Servers/UI/ImageComponents/ImagePower.razor index ccaca3bb..e94e1476 100644 --- a/Moonlight/Features/Servers/UI/ImageComponents/ImagePower.razor +++ b/Moonlight/Features/Servers/UI/ImageComponents/ImagePower.razor @@ -1,7 +1,7 @@ @using Moonlight.Features.Servers.Models.Forms.Admin.Images
-
+
@*
-
+
*@
diff --git a/Moonlight/Features/Servers/UI/ImageComponents/ImageVariables.razor b/Moonlight/Features/Servers/UI/ImageComponents/ImageVariables.razor index d4a1ae6f..e9e4dff5 100644 --- a/Moonlight/Features/Servers/UI/ImageComponents/ImageVariables.razor +++ b/Moonlight/Features/Servers/UI/ImageComponents/ImageVariables.razor @@ -1,52 +1,137 @@ @using Moonlight.Features.Servers.Entities -@using Moonlight.Features.Servers.Models.Forms.Admin.Images.Variables -@using BlazorTable +@using MoonCore.Abstractions +@using MoonCore.Exceptions +@using Moonlight.Features.Servers.Entities.Enums - +@inject Repository ImageRepository +@inject Repository VariableRepository + + - - - - - - + + + + + + - - + + - + - - - Add a new variable in order to get started. Need help? Check out our documentation - - - + @code { [Parameter] public ServerImage Image { get; set; } + + private IEnumerable Loader(Repository _) + { + return Image.Variables; + } + + private void OnConfigure(FastCrudConfiguration configuration) + { + configuration.CustomCreate = variable => + { + Image.Variables.Add(variable); + ImageRepository.Update(Image); + + return Task.CompletedTask; + }; + + configuration.CustomDelete = variable => + { + Image.Variables.Remove(variable); + ImageRepository.Update(Image); + + try + { + VariableRepository.Delete(variable); + } + catch (Exception) + { + /* dont fail here */ + } + + return Task.CompletedTask; + }; + + configuration.ValidateCreate = variable => + { + if (Image.Variables.Any(x => x.Key == variable.Key)) + throw new DisplayException("A variable with this key already exists"); + + return Task.CompletedTask; + }; + + configuration.ValidateEdit = variable => + { + if (Image.Variables.Any(x => x.Key == variable.Key && x.Id != variable.Id)) + throw new DisplayException("A variable with this key already exists"); + + return Task.CompletedTask; + }; + } + + private void OnConfigureForm(FastFormConfiguration configuration, ServerImageVariable _) + { + configuration.AddProperty(x => x.Key) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithDescription("This is the environment variable name"); + + configuration.AddProperty(x => x.DefaultValue) + .WithDefaultComponent() + .WithDescription("This is the default value which will be set when a server is created"); + + configuration.AddProperty(x => x.DisplayName) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithDescription("This is the display name of the variable which will be shown to the user if enabled to edit/view the variable"); + + configuration.AddProperty(x => x.Description) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithDescription("This text should describe what the variable does for the user if allowed to view and/or change"); + + configuration.AddProperty(x => x.AllowView) + .WithComponent() + .WithDescription("Allow the user to view the variable but not edit it unless specified otherwise"); + + configuration.AddProperty(x => x.AllowEdit) + .WithComponent() + .WithDescription("Allow the user to edit the variable. Wont work if view is disabled"); + + configuration.AddProperty(x => x.Type) + .WithComponent>() + .WithDescription("Specifies the type of the variable. This specifies what ui the user will see for the variable. You can also specify the options which are available using the filter field"); + + configuration.AddProperty(x => x.Filter) + .WithDefaultComponent() + .WithDescription("(Optional)\nText: A regex filter which will check if the user input mathes a correct variable value\nSelect: Specify the available values seperated by a semicolon"); + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor index 49eef3de..fd70716d 100644 --- a/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor +++ b/Moonlight/Features/Servers/UI/Layouts/UserLayout.razor @@ -3,8 +3,6 @@ @using Moonlight.Features.Servers.Services @using Moonlight.Features.Servers.UI.Components @using MoonCore.Abstractions -@using MoonCore.Helpers -@using MoonCoreUI.Services @using Microsoft.EntityFrameworkCore @using Moonlight.Features.Servers.Helpers @using Moonlight.Features.Servers.UI.UserViews @@ -12,8 +10,7 @@ @using System.Net.WebSockets @using MoonCore.Exceptions @using Moonlight.Features.Servers.Configuration -@using MoonCore.Services -@using Moonlight.Core.Services +@using MoonCore.Blazor.Forms.Router @inject Repository ServerRepository @inject ServerService ServerService @@ -21,10 +18,12 @@ @inject AlertService AlertService @inject IdentityService IdentityService @inject ConfigService ConfigService +@inject ILogger Logger +@inject ILoggerFactory LoggerFactory @implements IDisposable - +
@@ -43,21 +42,21 @@
@{ - var color = ServerUtilsHelper.GetColorFromState(Console.State); + var color = ServerUtilsHelper.GetColorFromState(Console.State); } - @(Console.State) + @(Console.State) (@(Formatter.FormatUptime(DateTime.UtcNow - Console.LastStateChangeTimestamp))) - +
- - - @(Server.Node.Fqdn):@(Server.MainAllocation.Port) - + + + @(Server.Node.Fqdn):@(Server.MainAllocation.Port) +
@*
@@ -73,47 +72,47 @@
@if (Console.State == ServerState.Offline) { - - - + + + } else { - + } @if (Console.State == ServerState.Offline || Console.State == ServerState.Installing) { - + } else { - - - + + + } @if (Console.State == ServerState.Offline || Console.State == ServerState.Installing) { - + } else { - - - + + + }
@@ -154,7 +153,7 @@ { - + @@ -186,7 +185,7 @@ - + } @@ -224,7 +223,7 @@ .Include(x => x.Owner) .First(x => x.Id == Id); - if (Server.Owner.Id != IdentityService.CurrentUser.Id && IdentityService.CurrentUser.Permissions < 5000) + if (Server.Owner.Id != IdentityService.GetUser().Id && IdentityService.GetUser().Permissions < 5000) { Server = null!; return; @@ -233,7 +232,7 @@ await lazyLoader.SetText("Establishing a connection to the server"); // Create console wrapper - Console = new ServerConsole(Server); + Console = new ServerConsole(Server, LoggerFactory); // Configure Console.OnStateChange += async state => await HandleStateChange(state); @@ -266,7 +265,7 @@ if (httpRequestException.InnerException is not SocketException socketException) throw; - Logger.Warn($"Unable to access the node's websocket endpoint: {socketException.Message}"); + Logger.LogWarning("Unable to access the node's websocket endpoint: {socketException}", socketException); // Change the ui and... IsNodeOffline = true; @@ -328,8 +327,13 @@ { if (!ConfigService.Get().DisableServerKillWarning) { - if (!await AlertService.YesNo("Do you really want to kill the server? This can result in data loss or corrupted server files")) - return; + await AlertService.Confirm( + "Server kill confirmation", + "Do you really want to kill the server? This can result in data loss or corrupted server files", + async () => await SendSignalHandled(PowerAction.Kill) + ); + + return; } await SendSignalHandled(PowerAction.Kill); @@ -347,8 +351,7 @@ } catch (Exception e) { - Logger.Warn($"An error occured while sending power action {action} to server {Server.Id}:"); - Logger.Warn(e); + Logger.LogWarning("An error occured while sending power action {action} to server {serverId}: {e}", action, Server.Id, e); await ToastService.Danger("An error occured while sending power action to server. Check the console for more information"); } diff --git a/Moonlight/Features/Servers/UI/NodeComponents/NodeAllocations.razor b/Moonlight/Features/Servers/UI/NodeComponents/NodeAllocations.razor index b93b9722..ac10ad0b 100644 --- a/Moonlight/Features/Servers/UI/NodeComponents/NodeAllocations.razor +++ b/Moonlight/Features/Servers/UI/NodeComponents/NodeAllocations.razor @@ -1,9 +1,8 @@ +@using System.ComponentModel.DataAnnotations @using Moonlight.Features.Servers.Entities -@using Moonlight.Features.Servers.Models.Forms.Admin.Nodes -@using BlazorTable @using MoonCore.Abstractions @using MoonCore.Exceptions -@using MoonCoreUI.Services + @inject Repository NodeRepository @inject Repository AllocationRepository @@ -27,38 +26,31 @@
- + + Add +
- - - + + + + + + + + - - - - - - - - - In order for a server to be deployed on this node allocations need to be created here - - - + +
@@ -66,8 +58,7 @@ { [Parameter] public ServerNode Node { get; set; } - // A bit long, lol - private AutoListCrud Crud; + private FastCrud Crud; // Quick add values private string IpAddress = "0.0.0.0"; @@ -101,53 +92,113 @@ NodeRepository.Update(Node!); await ToastService.Success($"Added {added} allocations and skipped {skipped} ports due to existing allocations"); - await Crud.Reload(); + await Crud.Refresh(fullRefresh: true); } private async Task DeleteAllAllocations() { - if (!await AlertService.YesNo("Do you really want to delete all allocations?", "Yes", "No")) - return; - - foreach (var allocation in Node!.Allocations.ToArray()) // To array in order to prevent collection modified exception + await AlertService.Confirm("Confirm mass deletion", "Do you really want to delete all allocations?", async () => { - // Check if a server is using this allocation before deleting - - if (ServerRepository - .Get() - .Any(x => x.Allocations.Any(y => y.Id == allocation.Id))) + foreach (var allocation in Node!.Allocations.ToArray()) // To array in order to prevent collection modified exception { - await ToastService.Danger($"Unable to delete allocation with port {allocation.Port} due to a server using this allocation"); - continue; + // Check if a server is using this allocation before deleting + + if (ServerRepository + .Get() + .Any(x => x.Allocations.Any(y => y.Id == allocation.Id))) + { + await ToastService.Danger($"Unable to delete allocation with port {allocation.Port} due to a server using this allocation"); + continue; + } + + AllocationRepository.Delete(allocation); } - AllocationRepository.Delete(allocation); - } - - await ToastService.Success("Successfully deleted allocations"); - await Crud.Reload(); + await ToastService.Success("Successfully deleted allocations"); + await Crud.Refresh(fullRefresh: true); + }); } - private Task ValidateDelete(ServerAllocation allocation) + private IEnumerable Loader(Repository _) { - // Check if allocation is associated with a server - var serverWithThisAllocation = ServerRepository - .Get() - .FirstOrDefault(x => x.Allocations.Any(y => y.Id == allocation.Id)); + return Node.Allocations; + } - if (serverWithThisAllocation != null) + private void OnConfigure(FastCrudConfiguration configuration) + { + configuration.ValidateCreate = allocation => { - throw new DisplayException($"The server '{serverWithThisAllocation.Name}' (ID: {serverWithThisAllocation.Id}) is using this allocation. Delete the server in order to delete this allocation"); - } + if (Node.Allocations.Any(x => x.Port == allocation.Port && x.IpAddress == allocation.IpAddress)) + throw new DisplayException("A allocation with these ip and port does already exist"); + + return Task.CompletedTask; + }; - return Task.CompletedTask; + configuration.ValidateEdit = allocation => + { + if (Node.Allocations.Any(x => x.Port == allocation.Port && x.IpAddress == allocation.IpAddress && x.Id != allocation.Id)) + throw new DisplayException("A allocation with these ip and port does already exist"); + + return Task.CompletedTask; + }; + + configuration.ValidateDelete = allocation => + { + // Check if allocation is associated with a server + var serverWithThisAllocation = ServerRepository + .Get() + .FirstOrDefault(x => x.Allocations.Any(y => y.Id == allocation.Id)); + + if (serverWithThisAllocation != null) + { + throw new DisplayException($"The server '{serverWithThisAllocation.Name}' (ID: {serverWithThisAllocation.Id}) is using this allocation. Delete the server in order to delete this allocation"); + } + + return Task.CompletedTask; + }; + + configuration.CustomCreate = allocation => + { + Node.Allocations.Add(allocation); + NodeRepository.Update(Node); + + return Task.CompletedTask; + }; + + /* + configuration.CustomEdit = allocation => + { + AllocationRepository.Update(allocation); + return Task.CompletedTask; + };*/ + + configuration.CustomDelete = allocation => + { + Node.Allocations.Remove(allocation); + NodeRepository.Update(Node); + + try + { + AllocationRepository.Delete(allocation); + } + catch (Exception) + { + /* do not fail here */ + } + + return Task.CompletedTask; + }; } - private Task ValidateAdd(ServerAllocation allocation) + private void OnConfigureForm(FastFormConfiguration configuration, ServerAllocation _) { - if (Node!.Allocations.Any(x => x.Port == allocation.Port && x.IpAddress == allocation.IpAddress)) - throw new DisplayException("A allocation with these ip and port does already exist"); + configuration.AddProperty(x => x.IpAddress) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", "You need to provide a valid ipv4 address")); - return Task.CompletedTask; + configuration.AddProperty(x => x.Port) + .WithDefaultComponent() + .WithValidation(x => x >= 1 && x <= 65535 ? ValidationResult.Success : new ValidationResult("You need to provide a valid port")); } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/NodeComponents/NodeLogs.razor b/Moonlight/Features/Servers/UI/NodeComponents/NodeLogs.razor index 38da293e..542c69fb 100644 --- a/Moonlight/Features/Servers/UI/NodeComponents/NodeLogs.razor +++ b/Moonlight/Features/Servers/UI/NodeComponents/NodeLogs.razor @@ -1,13 +1,14 @@ @using MoonCore.Helpers @using Moonlight.Features.Servers.Entities @using MoonCore.Services -@using MoonCoreUI.Services + @using Moonlight.Core.Configuration @using Moonlight.Features.Servers.Services @inject ConfigService ConfigService @inject NodeService NodeService @inject ToastService ToastService +@inject ILogger Logger
@@ -69,8 +70,7 @@ } catch (Exception e) { - Logger.Warn($"An error occured while fetching logs from node '{Node.Name}'"); - Logger.Warn(e); + Logger.LogWarning("An error occured while fetching logs from node '{name}': {e}", Node.Name, e); await ToastService.Danger("An error occured while fetching logs. Please try again later"); } diff --git a/Moonlight/Features/Servers/UI/NodeComponents/NodeOverview.razor b/Moonlight/Features/Servers/UI/NodeComponents/NodeOverview.razor index 13ca6920..302cd7c1 100644 --- a/Moonlight/Features/Servers/UI/NodeComponents/NodeOverview.razor +++ b/Moonlight/Features/Servers/UI/NodeComponents/NodeOverview.razor @@ -8,7 +8,7 @@ @implements IDisposable -
+
@{ var cpuName = Status.Hardware.Cores.Any() ? Status.Hardware.Cores.First().Name : "N/A"; diff --git a/Moonlight/Features/Servers/UI/NodeComponents/NodeSetup.razor b/Moonlight/Features/Servers/UI/NodeComponents/NodeSetup.razor index 447a9d1a..59619d17 100644 --- a/Moonlight/Features/Servers/UI/NodeComponents/NodeSetup.razor +++ b/Moonlight/Features/Servers/UI/NodeComponents/NodeSetup.razor @@ -1,7 +1,5 @@ @using Moonlight.Features.Servers.Entities -@using MoonCore.Services @using Moonlight.Core.Configuration -@using Moonlight.Core.Services @inject ConfigService ConfigService @inject IdentityService IdentityService @@ -36,6 +34,6 @@ $"--use-ftp-port {Node.FtpPort} " + $"--use-fqdn {Node.Fqdn} " + $"--use-ssl {Node.Ssl.ToString().ToLower()} " + - $"--use-email {IdentityService.CurrentUser.Email}"; + $"--use-email {IdentityService.GetUser().Email}"; } } diff --git a/Moonlight/Features/Servers/UI/UserViews/Backups.razor b/Moonlight/Features/Servers/UI/UserViews/Backups.razor index bcba5200..9c9fdc69 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Backups.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Backups.razor @@ -2,7 +2,7 @@ @using MoonCore.Abstractions @using Microsoft.EntityFrameworkCore @using MoonCore.Helpers -@using MoonCoreUI.Services + @using Moonlight.Features.Servers.Events @using Moonlight.Features.Servers.Helpers @using Moonlight.Features.Servers.Models.Enums @@ -13,13 +13,14 @@ @inject ToastService ToastService @inject AlertService AlertService @inject NavigationManager Navigation +@inject ServerEvents ServerEvents @implements IDisposable
- + Create backup
@@ -171,23 +172,23 @@ public async Task Restore(ServerBackup backup) { - if(!await AlertService.YesNo("Do you really want to restore this backup? All files on the server will be deleted and replaced by the backup")) - return; - - await ServerService.Backup.Restore(Server, backup); + await AlertService.Confirm("Confirm backup restore", "Do you really want to restore this backup? All files on the server will be deleted and replaced by the backup", async () => + { + await ServerService.Backup.Restore(Server, backup); - await ToastService.Success("Successfully restored backup"); + await ToastService.Success("Successfully restored backup"); + }); } public async Task Delete(ServerBackup backup, bool safeDelete = true) { - if(!await AlertService.YesNo("Do you really want to delete this backup? Deleted backups cannot be restored")) - return; - - await ServerService.Backup.Delete(Server, backup, safeDelete); + await AlertService.Confirm("Confirm backup deletion", "Do you really want to delete this backup? Deleted backups cannot be restored", async () => + { + await ServerService.Backup.Delete(Server, backup, safeDelete); - await ToastService.Success("Successfully deleted backup"); - await LazyLoader.Reload(); + await ToastService.Success("Successfully deleted backup"); + await LazyLoader.Reload(); + }); } private async Task Download(ServerBackup backup) diff --git a/Moonlight/Features/Servers/UI/UserViews/Console.razor b/Moonlight/Features/Servers/UI/UserViews/Console.razor index fe50781a..9b0d1bf4 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Console.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Console.razor @@ -17,7 +17,7 @@
- + Execute
diff --git a/Moonlight/Features/Servers/UI/UserViews/Files.razor b/Moonlight/Features/Servers/UI/UserViews/Files.razor index 6f05f72f..79befcfb 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Files.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Files.razor @@ -11,7 +11,7 @@ @implements IDisposable - + diff --git a/Moonlight/Features/Servers/UI/UserViews/Network.razor b/Moonlight/Features/Servers/UI/UserViews/Network.razor index 7634372e..4de96d38 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Network.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Network.razor @@ -3,7 +3,7 @@ @using Microsoft.EntityFrameworkCore @using MoonCore.Abstractions @using MoonCore.Helpers -@using MoonCoreUI.Services + @inject Repository NetworkRepository @inject Repository ServerRepository @@ -33,8 +33,8 @@
@if (!Server.DisablePublicNetwork) { - - + + - - + + - - - + + + - - + + - - + + } else { diff --git a/Moonlight/Features/Servers/UI/UserViews/Reset.razor b/Moonlight/Features/Servers/UI/UserViews/Reset.razor index 740e7607..b68a7eba 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Reset.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Reset.razor @@ -3,7 +3,7 @@ @using Moonlight.Features.Servers.Models.Abstractions @using Moonlight.Features.Servers.Models.Enums @using Moonlight.Features.Servers.Services -@using MoonCoreUI.Services + @implements IDisposable @@ -20,7 +20,7 @@

@if (Console.State == ServerState.Offline) { - + Reinstall } else { @@ -35,7 +35,7 @@

@if (Console.State == ServerState.Offline) { - + Reset } else { @@ -43,14 +43,15 @@ }
-
@* TODO: Make deleting configurable to show or not *@ +
+ @* TODO: Make deleting configurable to show or not *@

This deletes your server. The deleted data is not recoverable. Please make sure you have a backup of the data before deleting the server

@if (Console.State == ServerState.Offline) { - + Delete } else { @@ -78,50 +79,48 @@ private async Task Reinstall() { - if (!await AlertService.YesNo("Do you want to reinstall this server? This may replace/delete some files")) - return; - - await ServerService.Console.SendAction(Server, PowerAction.Install); + await AlertService.Confirm("Confirm reinstall", "Do you want to reinstall this server? This may replace/delete some files", async () => { await ServerService.Console.SendAction(Server, PowerAction.Install); }); } private async Task ResetServer() { - if (!await AlertService.YesNo("Do you want to reset this server? This will delete all files and run the install script")) - return; - - await ToastService.CreateProgress("serverReset", "Reset: Deleting files"); - - using var fileAccess = await ServerService.OpenFileAccess(Server); - - var files = await fileAccess.List(); - int i = 0; - - foreach (var fileEntry in files) + await AlertService.Confirm("Confirm server reset", "Do you want to reset this server? This will delete all files and run the install script", async () => { - i++; + await ToastService.CreateProgress("serverReset", "Reset: Deleting files"); - await ToastService.ModifyProgress("serverReset", $"Reset: Deleting files [{i} / {files.Length}]"); - await fileAccess.Delete(fileEntry); - } + using var fileAccess = await ServerService.OpenFileAccess(Server); - await ToastService.ModifyProgress("serverReset", "Reset: Starting install script"); + var files = await fileAccess.List(); + int i = 0; - await ServerService.Console.SendAction(Server, PowerAction.Install); + foreach (var fileEntry in files) + { + i++; - await ToastService.RemoveProgress("serverReset"); + await ToastService.UpdateProgress("serverReset", $"Reset: Deleting files [{i} / {files.Length}]"); + await fileAccess.Delete(fileEntry); + } + + await ToastService.UpdateProgress("serverReset", "Reset: Starting install script"); + + await ServerService.Console.SendAction(Server, PowerAction.Install); + + await ToastService.DeleteProgress("serverReset"); + }); } private async Task Delete() { - var input = await AlertService.Text($"Please type '{Server.Name}' to confirm deleting this server"); - - if(input != Server.Name) - return; + await AlertService.Text("Server deletion", $"Please type '{Server.Name}' to confirm deleting this server", async input => + { + if (input != Server.Name) + return; - await ServerService.Delete(Server); + await ServerService.Delete(Server); - await ToastService.Success("Successfully deleted server"); - Navigation.NavigateTo("/servers"); + await ToastService.Success("Successfully deleted server"); + Navigation.NavigateTo("/servers"); + }); } private async Task OnStateChanged(ServerState _) => await InvokeAsync(StateHasChanged); diff --git a/Moonlight/Features/Servers/UI/UserViews/Schedules.razor b/Moonlight/Features/Servers/UI/UserViews/Schedules.razor index 23099aa7..9f7e37fd 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Schedules.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Schedules.razor @@ -2,7 +2,7 @@ @using MoonCore.Abstractions @using Microsoft.EntityFrameworkCore @using MoonCore.Helpers -@using MoonCoreUI.Services + @using Moonlight.Features.Servers.Models.Forms.Users.Schedules @using Moonlight.Features.Servers.Services @using Newtonsoft.Json @@ -19,11 +19,13 @@
- + Create new schedule @foreach (var schedule in ServerWithSchedules.Schedules) { - + + @schedule.Name + }
@@ -158,15 +160,12 @@
- - @code { [CascadingParameter] public Server Server { get; set; } private Server ServerWithSchedules; private LazyLoader LazyLoader; - private FormModalLauncher Launcher; private ServerSchedule? SelectedSchedule; private List SortedItems = new(); @@ -208,11 +207,11 @@ private async Task CreateSchedule() { - await Launcher.Show("Create a new schedule", async form => + await AlertService.Text("New schedule", "Create a new schedule", async name => { ServerWithSchedules.Schedules.Add(new() { - Name = form.Name + Name = name }); ServerRepository.Update(ServerWithSchedules); @@ -243,7 +242,8 @@ return; } - await Launcher.Show("Configure action", async formData => { await AddScheduleAction(NewItemActionType, SelectedSchedule.Items.Count, formData); }, action.FormType); + // TODO: Redo everything here + //await Launcher.Show("Configure action", async formData => { await AddScheduleAction(NewItemActionType, SelectedSchedule.Items.Count, formData); }, action.FormType); } private async Task AddScheduleAction(string type, int priority, object data) @@ -276,7 +276,7 @@ var action = ScheduleService.Actions.First(x => x.Key == item.Action).Value; var formModel = JsonConvert.DeserializeObject(item.DataJson, action.FormType)!; - +/* await Launcher.Show("Configure action", async formData => { item.DataJson = JsonConvert.SerializeObject(formData); @@ -284,7 +284,7 @@ ScheduleItemRepository.Update(item); await ToastService.Success("Successfully updated action"); - }, action.FormType, formModel: formModel); + }, action.FormType, formModel: formModel);*/ } private async Task MoveItem(ServerScheduleItem item, int move) @@ -312,18 +312,18 @@ if (SelectedSchedule == null) return; - if (!await AlertService.YesNo("Do you really want to delete this action? This cannot be undone")) - return; + await AlertService.Confirm("Confirm schedule item deletion", "Do you really want to delete this action? This cannot be undone", async () => + { + SortedItems.Remove(item); + SelectedSchedule.Items.Remove(item); - SortedItems.Remove(item); - SelectedSchedule.Items.Remove(item); + ScheduleRepository.Update(SelectedSchedule); - ScheduleRepository.Update(SelectedSchedule); + await FixPriorities(); - await FixPriorities(); - - await ToastService.Success("Successfully deleted action"); - await LazyLoader.Reload(); + await ToastService.Success("Successfully deleted action"); + await LazyLoader.Reload(); + }); } private Task FixPriorities() @@ -346,27 +346,27 @@ if (SelectedSchedule == null) return; - if (!await AlertService.YesNo($"Do you really want to delete the schedule '{SelectedSchedule.Name}'? This cannot be undone")) - return; - - foreach (var item in SelectedSchedule.Items.ToArray()) + await AlertService.Confirm("Confirm schedule deletion", $"Do you really want to delete the schedule '{SelectedSchedule.Name}'? This cannot be undone", async () => { - try + foreach (var item in SelectedSchedule.Items.ToArray()) { - ScheduleItemRepository.Delete(item); + try + { + ScheduleItemRepository.Delete(item); + } + catch (Exception) + { + /* this should not fail the operation */ + } } - catch (Exception) - { - /* this should not fail the operation */ - } - } - ScheduleRepository.Delete(SelectedSchedule); + ScheduleRepository.Delete(SelectedSchedule); - SelectedSchedule = null; + SelectedSchedule = null; - await ToastService.Success("Successfully deleted schedule"); - await LazyLoader.Reload(); + await ToastService.Success("Successfully deleted schedule"); + await LazyLoader.Reload(); + }); } private async Task RunSelectedSchedule() @@ -378,7 +378,7 @@ var result = await ScheduleService.Run(Server, SelectedSchedule); - await ToastService.RemoveProgress("scheduleRun"); + await ToastService.DeleteProgress("scheduleRun"); if (result.Failed) await ToastService.Danger($"Schedule run failed ({result.ExecutionSeconds}s)"); diff --git a/Moonlight/Features/Servers/UI/UserViews/Variables.razor b/Moonlight/Features/Servers/UI/UserViews/Variables.razor index d96d9ff7..f91560c8 100644 --- a/Moonlight/Features/Servers/UI/UserViews/Variables.razor +++ b/Moonlight/Features/Servers/UI/UserViews/Variables.razor @@ -2,7 +2,7 @@ @using Moonlight.Features.Servers.UI.Components.VariableViews @using MoonCore.Abstractions @using Microsoft.EntityFrameworkCore -@using MoonCoreUI.Services + @using Moonlight.Features.Servers.Entities.Enums @inject Repository ServerRepository @@ -10,14 +10,14 @@ @inject Repository ServerVariableRepository @inject ToastService ToastService - +
@if (Image.AllowDockerImageChange) { - diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor index 6275b273..120dc80b 100644 --- a/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor +++ b/Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor @@ -1,15 +1,15 @@ @page "/admin/servers/images" +@using System.ComponentModel.DataAnnotations @using Moonlight.Features.Servers.UI.Components @using Microsoft.EntityFrameworkCore @using MoonCore.Abstractions @using MoonCore.Exceptions -@using MoonCoreUI.Services + @using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Helpers -@using Moonlight.Features.Servers.Models.Forms.Admin.Images @using Microsoft.AspNetCore.Components.Forms -@using MoonCore.Helpers +@using Moonlight.Features.Servers.UI.ImageComponents @inject Repository ServerRepository @inject Repository VariableRepository @@ -17,31 +17,29 @@ @inject Repository ImageRepository @inject ImageConversionHelper ImageConversionHelper -@inject FileDownloadService FileDownloadService +@inject DownloadService DownloadService @inject ToastService ToastService @inject AlertService AlertService +@inject ILogger Logger @attribute [RequirePermission(5002)] - + - - + + - - - + + + - + - - - Download and import a image from our repository or create a new one. Need help? Check out our documentation - - - - - + + + Import egg - - + + Import - - - + + + @code { - private AutoCrud Crud; - private SmartCustomFileSelect ImageUpload; - private SmartCustomFileSelect EggUpload; + private FastCrud Crud; - private IEnumerable Load(Repository repository) + private MCBCustomFileSelect ImageUpload; + private MCBCustomFileSelect EggUpload; + + private IEnumerable Loader(Repository repository) { - return repository.Get(); + return repository + .Get() + .Include(x => x.DockerImages) + .Include(x => x.Variables); } - private Task ValidateDelete(ServerImage serverImage) + private void OnConfigure(FastCrudConfiguration configuration) { - if (ServerRepository.Get().Any(x => x.Image.Id == serverImage.Id)) - throw new DisplayException("A server using this image exists. Please delete the servers using this image to continue"); + configuration.ValidateDelete = image => + { + if (ServerRepository.Get().Any(x => x.Image.Id == image.Id)) + throw new DisplayException("A server using this image exists. Please delete the servers using this image to continue"); - return Task.CompletedTask; + return Task.CompletedTask; + }; + + configuration.CustomDelete = CustomDelete; } - private Task ValidateAdd(ServerImage image) + private void OnConfigureForm(FastFormConfiguration configuration, ServerImage image) { - // Set defaults + // General + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithPage("General") + .WithValidation(FastFormValidators.Required); - image.StopCommand = "^C"; - image.StartupCommand = "echo Startup command here"; - image.AllocationsNeeded = 1; - image.InstallScript = "#! /bin/bash\necho Done"; - image.InstallShell = "/bin/bash"; - image.InstallDockerImage = "debian:latest"; - image.OnlineDetection = "Running"; - image.AllowDockerImageChange = false; - image.DefaultDockerImage = 0; - image.ParseConfiguration = "[]"; + configuration.AddProperty(x => x.Author) + .WithDefaultComponent() + .WithPage("General") + .WithValidation(FastFormValidators.Required); - return Task.CompletedTask; + configuration.AddProperty(x => x.DonateUrl) + .WithDefaultComponent() + .WithPage("General") + .WithDescription("Provide a url here in order to give people the ability to donate for your work"); + + configuration.AddProperty(x => x.UpdateUrl) + .WithDefaultComponent() + .WithPage("General") + .WithDescription("A http(s) url directly to a json file which will serve as an update for the image. When a update is fetched, it will just get this url and try to load it"); + + // Power + configuration.AddProperty(x => x.StartupCommand) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithPage("Start, Stop & Status") + .WithDescription("This command will be executed at the start of a server. You can use environment variables in a {} here"); + + configuration.AddProperty(x => x.OnlineDetection) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithPage("Start, Stop & Status") + .WithDescription("A regex string specifying that a server is online when the daemon finds a match in the console output matching this expression"); + + configuration.AddProperty(x => x.StopCommand) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required) + .WithPage("Start, Stop & Status") + .WithDescription("A command which will be sent to the servers stdin when it should get stopped. Power signals can be achived by using ^. E.g. ^C"); + + // Parsing + configuration.AddProperty(x => x.ParseConfiguration) + .WithComponent() + .WithPage("Parsing"); + + configuration.AddCustomPage("Variables", ComponentHelper.FromType(parameters => + { + parameters.Add("Image", image); + })); + + configuration.AddCustomPage("Docker Images", ComponentHelper.FromType(parameters => + { + parameters.Add("Image", image); + })); + + configuration.AddProperty(x => x.AllowDockerImageChange) + .WithComponent() + .WithPage("Miscellaneous") + .WithDescription("This toggle specifies if a user is allowed to change the docker image from the list of docker images associated to the image"); + + configuration.AddProperty(x => x.DefaultDockerImage) + .WithComponent(dockerImage => + { + dockerImage.Image = image; + }) + .WithPage("Miscellaneous"); + + configuration.AddProperty(x => x.AllocationsNeeded) + .WithDefaultComponent() + .WithPage("Miscellaneous") + .WithValidation(x => x > 1 ? ValidationResult.Success : new ValidationResult("This specifies the amount of allocations needed for this image in order to create a server")); + + configuration.AddProperty(x => x.InstallDockerImage) + .WithDefaultComponent() + .WithPage("Installation") + .WithName("Docker Image") + .WithValidation(FastFormValidators.Required) + .WithValidation(RegexValidator.Create("^(?:[a-zA-Z0-9\\-\\.]+\\/)?[a-zA-Z0-9\\-]+(?:\\/[a-zA-Z0-9\\-]+)*(?::[a-zA-Z0-9_\\.-]+)?$", "You need to provide a valid docker image name")); + + configuration.AddProperty(x => x.InstallShell) + .WithDefaultComponent() + .WithPage("Installation") + .WithName("Shell") + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.InstallScript) + .WithComponent() + .WithPage("Installation") + .WithName("Script") + .WithValidation(FastFormValidators.Required); } private Task CustomDelete(ServerImage serverImage) @@ -178,7 +256,7 @@ { var json = await ImageConversionHelper.ExportAsJson(image); var imageName = image.Name.Replace(" ", ""); - await FileDownloadService.DownloadString($"{imageName}.json", json); + await DownloadService.DownloadString($"{imageName}.json", json); await ToastService.Success($"Successfully exported '{image.Name}'"); } @@ -198,7 +276,7 @@ await ToastService.Success($"Successfully imported '{image.Name}'"); await ImageUpload.RemoveSelection(); - await Crud.Reload(); + await Crud.Refresh(); } catch (DisplayException) { @@ -206,8 +284,7 @@ } catch (Exception e) { - Logger.Warn("An error occured while importing a image"); - Logger.Warn(e); + Logger.LogWarning("An error occured while importing a image: {e}", e); await ToastService.Danger("Unable to import egg: " + e.Message); } @@ -219,45 +296,39 @@ private async Task ImportEgg(IBrowserFile file) { - var confirm = await AlertService.YesNo("Importing pterodactyl eggs is a experimental feature and may result in unusable images. Are you sure you want to proceed?", - "Yes, i take the risk", - "Cancel"); + await AlertService.Confirm("Import a pterodactyl egg", "Importing pterodactyl eggs is a experimental feature and may result in unusable images. Are you sure you want to proceed?", + async () => + { + try + { + var stream = file.OpenReadStream(); - if (!confirm) - { - await EggUpload.RemoveSelection(); - return; - } + using var sr = new StreamReader(stream); + var content = await sr.ReadToEndAsync(); - try - { - var stream = file.OpenReadStream(); + var image = await ImageConversionHelper.ImportFromEggJson(content); - using var sr = new StreamReader(stream); - var content = await sr.ReadToEndAsync(); + ImageRepository.Add(image); + await ToastService.Success($"Successfully imported '{image.Name}'"); - var image = await ImageConversionHelper.ImportFromEggJson(content); + await EggUpload.RemoveSelection(); + await Crud.Refresh(); + } + catch (DisplayException) + { + throw; + } + catch (Exception e) + { + Logger.LogWarning("An error occured while importing a pterodactyl egg: {e}", e); - ImageRepository.Add(image); - await ToastService.Success($"Successfully imported '{image.Name}'"); - - await EggUpload.RemoveSelection(); - await Crud.Reload(); - } - catch (DisplayException) - { - throw; - } - catch (Exception e) - { - Logger.Warn("An error occured while importing a pterodactyl egg"); - Logger.Warn(e); - - await ToastService.Danger("Unable to import egg: " + e.Message); - } - finally - { - await EggUpload.RemoveSelection(); - } + await ToastService.Danger("Unable to import egg: " + e.Message); + } + finally + { + await EggUpload.RemoveSelection(); + } + }, + "Yes, i take the risk"); } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Images/View.razor b/Moonlight/Features/Servers/UI/Views/Admin/Images/View.razor index 83599c99..6a3b9357 100644 --- a/Moonlight/Features/Servers/UI/Views/Admin/Images/View.razor +++ b/Moonlight/Features/Servers/UI/Views/Admin/Images/View.razor @@ -1,7 +1,7 @@ @page "/admin/servers/images/view/{Id:int}/{Route?}" @using Mappy.Net -@using MoonCoreUI.Services + @using MoonCore.Abstractions @using Moonlight.Features.Servers.Entities @using Microsoft.EntityFrameworkCore diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor index 177ebe9b..cdca0ee5 100644 --- a/Moonlight/Features/Servers/UI/Views/Admin/Index.razor +++ b/Moonlight/Features/Servers/UI/Views/Admin/Index.razor @@ -1,102 +1,63 @@ @page "/admin/servers" +@using System.ComponentModel.DataAnnotations @using Moonlight.Features.Servers.UI.Components @using Microsoft.EntityFrameworkCore @using MoonCore.Abstractions @using MoonCore.Exceptions -@using MoonCore.Helpers -@using MoonCoreUI.Models -@using MoonCoreUI.Services +@using Moonlight.Core.Database.Entities + @using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Models.Enums -@using Moonlight.Features.Servers.Models.Forms.Admin.Servers @using Moonlight.Features.Servers.Services @inject ServerService ServerService @inject Repository ServerRepository +@inject Repository AllocRepository @inject ToastService ToastService +@inject AlertService AlertService +@inject ILogger Logger @attribute [RequirePermission(5000)] - + - - - + + + - - + + - - + + - + - - - Create a new server in order to manage it using this page. Need help? Check out our documentation - - - + + + + Force delete + + + @code { - private void OnConfigure(AutoCrudOptions options) - { - options.AddCustomItemLoader("FreeAllocations", LoadFreeAllocations); - - options.AddCustomDisplayFunction("AllocationWithIp", - allocation => allocation.IpAddress + ":" + allocation.Port); - } - - private IEnumerable Load(Repository repository) - { - return repository - .Get() - .Include(x => x.Owner) - .Include(x => x.Image) - .Include(x => x.Allocations) - .Include(x => x.Node); - } - - private IEnumerable LoadFreeAllocations(Repository repository, Server? currentServer) - { - if (currentServer == null) - { - return repository - .Get() - .FromSqlRaw("SELECT * FROM `ServerAllocations` WHERE ServerId IS NULL"); - } - else - { - return currentServer.Allocations.Concat( - repository - .Get() - .FromSqlRaw($"SELECT * FROM `ServerAllocations` WHERE ServerId IS NULL AND ServerNodeId = {currentServer.Node.Id}") - .AsEnumerable() // => executes the sql - ); - } - } - - private async Task CustomAdd(Server form) => await ServerService.Create(form); - - private async Task CustomDelete(Server s) => await ServerService.Delete(s); + private FastCrud Crud; private async Task CustomUpdate(Server server) { @@ -124,8 +85,7 @@ } catch (Exception e) { - Logger.Error("Unable to sync server changes due to an error occuring"); - Logger.Error(e); + Logger.LogError("Unable to sync server changes due to an error occuring: {e}", e); await ToastService.Danger("An error occured while sending the changes to the daemon"); } @@ -155,4 +115,143 @@ return Task.CompletedTask; } + + private async Task StartForceDelete(Server server) + { + await AlertService.Confirm( + "Confirm forcefully server deletion", + "Do you really want to delete this server forcefully?", + async () => + { + await ServerService.Delete(server, safeDelete: false); + await ToastService.Success("Successfully deleted server"); + + await Crud.SetState(FastCrudState.View); + } + ); + } + + private IEnumerable Loader(Repository repository) + { + return repository + .Get() + .Include(x => x.Image) + .Include(x => x.Node) + .Include(x => x.Owner) + .Include(x => x.Allocations); + } + + private void OnConfigure(FastCrudConfiguration configuration) + { + configuration.CustomCreate = ServerService.Create; + configuration.CustomDelete = server => ServerService.Delete(server); + configuration.CustomEdit = CustomUpdate; + + configuration.ValidateEdit = ValidateUpdate; + } + + // Shared form + private void OnConfigureBase(FastFormConfiguration configuration, Server server) + { + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Owner) + .WithComponent>(component => + { + component.SearchFunc = x => x.Username; + component.DisplayFunc = x => x.Username; + }) + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Image) + .WithComponent>(component => + { + component.SearchFunc = x => x.Name; + component.DisplayFunc = x => x.Name; + }) + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Cpu) + .WithDefaultComponent() + .WithValidation(x => x > 0 ? ValidationResult.Success : new("You need to provide a valid value")) + .WithSection("Resources", "bxs-chip") + .WithDescription("The cores the server will be able to use. 100 = 1 Core"); + + configuration.AddProperty(x => x.Memory) + .WithComponent(component => + { + component.MinimumUnit = "MB"; + component.DefaultUnit = "GB"; + component.Converter = 1; + }) + .WithValidation(x => x > 0 ? ValidationResult.Success : new("You need to provide a valid value")) + .WithSection("Resources") + .WithDescription("The amount of memory this server will be able to use"); + + configuration.AddProperty(x => x.Disk) + .WithComponent(component => + { + component.MinimumUnit = "MB"; + component.DefaultUnit = "GB"; + component.Converter = 1; + }) + .WithValidation(x => x > 0 ? ValidationResult.Success : new("You need to provide a valid value")) + .WithSection("Resources") + .WithDescription("The amount of disk space this server will be able to use"); + + configuration.AddProperty(x => x.UseVirtualDisk) + .WithComponent() + .WithPage("Advanced options") + .WithDescription("Whether to use a virtual disk for storing server files. Dont use this if you want to overallocate as the virtual disks will fill out the space you allocate"); + + configuration.AddProperty(x => x.DisablePublicNetwork) + .WithComponent() + .WithPage("Network") + .WithDescription("Whether to block all incoming connections to this server from the internet"); + + configuration.AddProperty(x => x.Allocations) + .WithComponent>(component => + { + component.SearchFunc = x => $"{x.IpAddress}:{x.Port}"; + component.DisplayFunc = x => $"{x.IpAddress}:{x.Port}"; + component.ItemsCallback = () =>GetAllocation(server); + component.ColumnsMd = 6; + }) + .WithPage("Network"); + } + + // Specific form + private void OnConfigureCreate(FastFormConfiguration configuration, Server server) + { + OnConfigureBase(configuration, server); + + configuration.AddProperty(x => x.Node) + .WithComponent>(component => + { + component.SearchFunc = x => x.Name; + component.DisplayFunc = x => x.Name; + }) + .WithValidation(FastFormValidators.Required); + } + + private void OnConfigureEdit(FastFormConfiguration configuration, Server server) + { + OnConfigureBase(configuration, server); + } + + private IEnumerable GetAllocation(Server server) + { + if (server == null) + return Array.Empty(); + + if (server.Node == null) + return Array.Empty(); + + return server.Allocations.Concat( + AllocRepository + .Get() + .FromSqlRaw($"SELECT * FROM `ServerAllocations` WHERE ServerId IS NULL AND ServerNodeId = {server.Node.Id}")); + } } \ No newline at end of file diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Manager.razor b/Moonlight/Features/Servers/UI/Views/Admin/Manager.razor index ebe8b8c9..b3a981bc 100644 --- a/Moonlight/Features/Servers/UI/Views/Admin/Manager.razor +++ b/Moonlight/Features/Servers/UI/Views/Admin/Manager.razor @@ -2,7 +2,7 @@ @using MoonCore.Abstractions @using MoonCore.Helpers -@using MoonCoreUI.Services + @using Moonlight.Features.Servers.Api.Resources @using Moonlight.Features.Servers.Entities @using Moonlight.Features.Servers.Services @@ -18,6 +18,7 @@ @inject ServerService ServerService @inject ToastService ToastService @inject AlertService AlertService +@inject ILogger Logger @attribute [RequirePermission(5000)] @@ -210,8 +211,7 @@ } catch (Exception e) { - Logger.Warn($"An error occured while fetching server list from node {node.Id}"); - Logger.Warn(e); + Logger.LogWarning("An error occured while fetching server list from node {nodeId}: {e}", node.Id, e); OfflineNodes.Add(node); @@ -248,33 +248,32 @@ await Table.SetPageSizeAsync(PageSize); // Confirm - if(!await AlertService.YesNo($"Do you really want to perform the action '{action}' for {items.Length} servers?")) - return; - - await ToastService.CreateProgress("multiPowerAction", "Preparing"); - - // Perform - int i = 0; - foreach (var item in items) + await AlertService.Confirm("Confirm power action", $"Do you really want to perform the action '{action}' for {items.Length} servers?", async () => { - try + await ToastService.CreateProgress("multiPowerAction", "Preparing"); + + // Perform + int i = 0; + foreach (var item in items) { - await ToastService.ModifyProgress("multiPowerAction", $"Sending power action [{i + 1} / {items.Length}]"); - await ServerService.Console.SendAction(item.Server, action); + try + { + await ToastService.UpdateProgress("multiPowerAction", $"Sending power action [{i + 1} / {items.Length}]"); + await ServerService.Console.SendAction(item.Server, action); - i++; - } - catch (Exception e) - { - Logger.Warn($"An error occured while performing power action on server {item.Server.Id}"); - Logger.Warn(e); + i++; + } + catch (Exception e) + { + Logger.LogWarning("An error occured while performing power action on server {serverId}: {e}", item.Server.Id, e); - await ToastService.Danger($"Unable to perform power action for server '{item.Server.Name}'"); + await ToastService.Danger($"Unable to perform power action for server '{item.Server.Name}'"); + } } - } - await ToastService.RemoveProgress("multiPowerAction"); - await ToastService.Success($"Successfully performed the action for {i} servers"); + await ToastService.DeleteProgress("multiPowerAction"); + await ToastService.Success($"Successfully performed the action for {i} servers"); + }); } public void Dispose() diff --git a/Moonlight/Features/Servers/UI/Views/Admin/Nodes/Index.razor b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/Index.razor index 1effa876..7183a53f 100644 --- a/Moonlight/Features/Servers/UI/Views/Admin/Nodes/Index.razor +++ b/Moonlight/Features/Servers/UI/Views/Admin/Nodes/Index.razor @@ -1,6 +1,4 @@ @page "/admin/servers/nodes" - -@using Moonlight.Features.Servers.Models.Forms.Admin.Nodes @using Moonlight.Features.Servers.UI.Components @using Moonlight.Features.Servers.Entities @using MoonCore.Abstractions @@ -10,11 +8,13 @@ @using System.Text.RegularExpressions; @using Moonlight.Features.Servers.Api.Resources @using Moonlight.Features.Servers.Services +@using Moonlight.Features.Servers.UI.NodeComponents @inject Repository ServerRepository @inject Repository NodeRepository @inject NodeService NodeService @inject IServiceProvider ServiceProvider +@inject ILogger Logger @implements IDisposable @@ -23,22 +23,22 @@ - + - - + + - - - + + + - - + + - - + + - - + + - + - - - Add a new node in order to get started. Need help? Check out our documentation - - - + @code @@ -124,79 +119,162 @@ { UpdateTimer = new(async _ => { - NodeStats.Clear(); - - using var scope = ServiceProvider.CreateScope(); - var nodeRepo = scope.ServiceProvider.GetRequiredService>(); - var nodes = nodeRepo.Get().ToArray(); - - foreach (var node in nodes) + try { - try + NodeStats.Clear(); + + using var scope = ServiceProvider.CreateScope(); + var nodeRepo = scope.ServiceProvider.GetRequiredService>(); + var nodes = nodeRepo.Get().ToArray(); + + foreach (var node in nodes) { - var status = await NodeService.GetStatus(node); + try + { + var status = await NodeService.GetStatus(node); - NodeStats[node.Id] = status; + NodeStats[node.Id] = status; + } + catch (Exception e) + { + Logger.LogWarning("Unable to fetch system status for node '{name}': {e}", node.Name, e); + + NodeStats[node.Id] = null; + } + + await InvokeAsync(StateHasChanged); } - catch (Exception e) - { - Logger.Warn($"Unable to fetch system status for node '{node.Name}'"); - Logger.Warn(e); - - NodeStats[node.Id] = null; - } - - await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + Logger.LogError("Unable to update node stats due to an unhandled error: {e}", e); } }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); return Task.CompletedTask; } - - private IEnumerable LoadData(Repository repository) + + private IEnumerable Loader(Repository repository) { return repository - .Get(); - } - - private Task ValidateDelete(ServerNode node) - { - if (ServerRepository .Get() - .Any(x => x.Node.Id == node.Id)) - { - throw new DisplayException("There are still servers on this node. Delete the servers in order to delete the node"); - } - - if (NodeRepository - .Get() - .Include(x => x.Allocations) - .First(x => x.Id == node.Id) - .Allocations - .Any()) - { - throw new DisplayException("There are still allocations on this node. Delete the allocations in order to delete the node"); - } - - return Task.CompletedTask; + .Include(x => x.Allocations); } - private Task ValidateAdd(ServerNode node) + private void OnConfigure(FastCrudConfiguration configuration) { - ValidateFqdn(node); + configuration.ValidateDelete = node => + { + if (ServerRepository + .Get() + .Any(x => x.Node.Id == node.Id)) + { + throw new DisplayException("There are still servers on this node. Delete the servers in order to delete the node"); + } - node.Token = Formatter.GenerateString(32); + if (NodeRepository + .Get() + .Include(x => x.Allocations) + .First(x => x.Id == node.Id) + .Allocations + .Any()) + { + throw new DisplayException("There are still allocations on this node. Delete the allocations in order to delete the node"); + } - return Task.CompletedTask; + return Task.CompletedTask; + }; + + configuration.ValidateCreate = node => + { + ValidateFqdn(node); + + node.Token = Formatter.GenerateString(32); + + return Task.CompletedTask; + }; + + configuration.ValidateEdit = node => + { + ValidateFqdn(node); + + return Task.CompletedTask; + }; } - private Task ValidateUpdate(ServerNode node) + private void OnConfigureCreate(FastFormConfiguration configuration, ServerNode _) { - ValidateFqdn(node); + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required); - return Task.CompletedTask; + configuration.AddProperty(x => x.Fqdn) + .WithDefaultComponent() + .WithDescription("This needs to be the ip or domain of the node depending on the ssl settings") + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Ssl) + .WithComponent() + .WithDescription("This enables ssl for the http connections to the node. Only enable this if you have the cert installed on the node"); + + configuration.AddProperty(x => x.HttpPort) + .WithDefaultComponent() + .WithDescription("This is the http(s) port used by the node to allow communication to the node from the panel"); + + configuration.AddProperty(x => x.FtpPort) + .WithDefaultComponent() + .WithDescription("This is the ftp port users can use to access their servers filesystem via their ftp client"); } + private void OnConfigureEdit(FastFormConfiguration configuration, ServerNode node) + { + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithPage("Settings") + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Fqdn) + .WithDefaultComponent() + .WithPage("Settings") + .WithDescription("This needs to be the ip or domain of the node depending on the ssl settings") + .WithValidation(FastFormValidators.Required); + + configuration.AddProperty(x => x.Ssl) + .WithComponent() + .WithPage("Settings") + .WithDescription("This enables ssl for the http connections to the node. Only enable this if you have the cert installed on the node"); + + configuration.AddProperty(x => x.HttpPort) + .WithDefaultComponent() + .WithPage("Settings") + .WithDescription("This is the http(s) port used by the node to allow communication to the node from the panel"); + + configuration.AddProperty(x => x.FtpPort) + .WithDefaultComponent() + .WithPage("Settings") + .WithDescription("This is the ftp port users can use to access their servers filesystem via their ftp client"); + + configuration.AddCustomPage("Overview", ComponentHelper.FromType(parameters => + { + parameters.Add("Node", node); + })); + + configuration.AddCustomPage("Allocations", ComponentHelper.FromType(parameters => + { + parameters.Add("Node", node); + })); + + configuration.AddCustomPage("Setup", ComponentHelper.FromType(parameters => + { + parameters.Add("Node", node); + })); + + configuration.AddCustomPage("Logs", ComponentHelper.FromType(parameters => + { + parameters.Add("Node", node); + })); + } + private void ValidateFqdn(ServerNode node) { if (node.Ssl) @@ -212,7 +290,7 @@ // Is it a valid domain? if (Regex.IsMatch(node.Fqdn, "^(?!-)(?:[a-zA-Z\\d-]{0,62}[a-zA-Z\\d]\\.)+(?:[a-zA-Z]{2,})$")) return; - + // Is it a valid ip? if (Regex.IsMatch(node.Fqdn, "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) return; @@ -220,7 +298,7 @@ throw new DisplayException("The fqdn needs to be either a domain or an ip"); } } - + public void Dispose() { UpdateTimer?.Dispose(); diff --git a/Moonlight/Features/Servers/UI/Views/Servers/Index.razor b/Moonlight/Features/Servers/UI/Views/Servers/Index.razor index 80007814..cd7d2f21 100644 --- a/Moonlight/Features/Servers/UI/Views/Servers/Index.razor +++ b/Moonlight/Features/Servers/UI/Views/Servers/Index.razor @@ -14,6 +14,7 @@ @inject Repository ServerRepository @inject IdentityService IdentityService @inject ServerService ServerService +@inject ILogger Logger @@ -142,7 +143,7 @@ .Include(x => x.MainAllocation) .Include(x => x.Node) .Include(x => x.Image) - .Where(x => x.Owner.Id == IdentityService.CurrentUser.Id) + .Where(x => x.Owner.Id == IdentityService.GetUser().Id) .ToArray(); Task.Run(async () => @@ -163,8 +164,7 @@ } catch (Exception e) { - Logger.Warn($"Unable to get server state for server {server.Id}"); - Logger.Warn(e); + Logger.LogWarning("Unable to get server state for server {serverId}: {e}", server.Id, e); } } }); @@ -187,8 +187,7 @@ } catch (Exception e) { - Logger.Warn($"Unable to get server stats for server {server.Id}"); - Logger.Warn(e); + Logger.LogWarning("Unable to get server stats for server {serverId}: {e}", server.Id, e); } } }); diff --git a/Moonlight/Features/Servers/UI/Views/Servers/Networks.razor b/Moonlight/Features/Servers/UI/Views/Servers/Networks.razor index 2c24bdba..7f44e9f2 100644 --- a/Moonlight/Features/Servers/UI/Views/Servers/Networks.razor +++ b/Moonlight/Features/Servers/UI/Views/Servers/Networks.razor @@ -1,8 +1,8 @@ @page "/servers/networks" +@using System.ComponentModel.DataAnnotations @using Moonlight.Features.Servers.UI.Components @using Moonlight.Features.Servers.Entities -@using Moonlight.Features.Servers.Models.Forms.Users.Networks @using MoonCore.Abstractions @using Moonlight.Core.Services @using Microsoft.EntityFrameworkCore @@ -13,50 +13,45 @@ - + - - + + - - + + - + - - - Create a new private network in order to connect multiple servers on the same node - - - + @code { private readonly Dictionary UsedByCache = new(); - private IEnumerable Load(Repository repository) + private IEnumerable Loader(Repository repository) { var result = repository .Get() .Include(x => x.Node) - .Where(x => x.User.Id == IdentityService.CurrentUser.Id); + .Where(x => x.User.Id == IdentityService.GetUser().Id); UsedByCache.Clear(); @@ -65,7 +60,7 @@ var serversUsingThisNetwork = ServerRepository .Get() .Where(x => x.Network.Id == network.Id) - .Where(x => x.Owner.Id == IdentityService.CurrentUser.Id) + .Where(x => x.Owner.Id == IdentityService.GetUser().Id) .ToArray(); UsedByCache.Add(network.Id, serversUsingThisNetwork); @@ -74,20 +69,44 @@ return result; } - private Task ValidateAdd(ServerNetwork network) + private void OnConfigure(FastCrudConfiguration configuration) { - if (!ServerRepository - .Get() - .Any(x => x.Node.Id == network.Node.Id && x.Owner.Id == IdentityService.CurrentUser.Id)) + configuration.ValidateCreate = network => { - throw new DisplayException("You need a server on the selected node in order to create a network on the node"); - } + if (!ServerRepository + .Get() + .Any(x => x.Node.Id == network.Node.Id && x.Owner.Id == IdentityService.GetUser().Id)) + { + throw new DisplayException("You need a server on the selected node in order to create a network on the node"); + } - //TODO: Add config to check the amount of networks created + //TODO: Add config to check the amount of networks created - // Set user as the crud is not allowed to set it (user crud and so on) - network.User = IdentityService.CurrentUser; + // Set user as the crud is not allowed to set it (user crud and so on) + network.User = IdentityService.GetUser(); + + return Task.CompletedTask; + }; + } + + private void OnConfigureCreate(FastFormConfiguration configuration, ServerNetwork _) + { + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required); - return Task.CompletedTask; + configuration.AddProperty(x => x.Node) + .WithComponent>(component => + { + component.DisplayField = x => x.Name; + }) + .WithValidation(x => x != null ? ValidationResult.Success : new ValidationResult("You need to specify a node")); + } + + private void OnConfigureEdit(FastFormConfiguration configuration, ServerNetwork _) + { + configuration.AddProperty(x => x.Name) + .WithDefaultComponent() + .WithValidation(FastFormValidators.Required); } } \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index bfd080f9..b3ecef82 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -80,25 +80,25 @@ - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Moonlight/Pages/_Host.cshtml b/Moonlight/Pages/_Host.cshtml index bb49475b..183c616b 100644 --- a/Moonlight/Pages/_Host.cshtml +++ b/Moonlight/Pages/_Host.cshtml @@ -32,6 +32,8 @@ + + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 6a553486..3754c236 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography.X509Certificates; using MoonCore.Extensions; using MoonCore.Helpers; +using MoonCore.Logging; using MoonCore.Services; using Moonlight.Core.Configuration; using Moonlight.Core.Database; @@ -19,25 +20,41 @@ var configService = new ConfigService( PathBuilder.File("storage", "configs", "core.json") ); +// Build pre run logger +var loggerProviders = LoggerBuildHelper.BuildFromConfiguration(new() +{ + Console = new() + { + Enable = true, + EnableAnsiMode = true + }, + FileLogging = new() + { + Enable = true, + Path = PathBuilder.File("storage", "logs", "moonlight.log"), + EnableLogRotation = true, + RotateLogNameTemplate = PathBuilder.File("storage", "logs", "moonlight.{0}.log") + } +}); + +var preRunLoggerFactory = new LoggerFactory(); +preRunLoggerFactory.AddProviders(loggerProviders); +var preRunLogger = preRunLoggerFactory.CreateLogger(); + +preRunLogger.LogInformation("Initializing moonlight"); + +// Initialisation var builder = WebApplication.CreateBuilder(args); -// Setup logging -Logger.Setup( - logInConsole: true, - logInFile: true, - logPath: PathBuilder.File("storage", "logs", "moonlight.log"), - isDebug: builder.Environment.IsDevelopment(), - enableFileLogRotate: true, - rotateLogNameTemplate: PathBuilder.File("storage", "logs", "moonlight.{0}.log") -); - -builder.Logging.MigrateToMoonCore(); +builder.Logging.ClearProviders(); +builder.Logging.AddProviders(loggerProviders); builder.Logging.AddConfiguration("{\"LogLevel\":{\"Default\":\"Information\",\"Microsoft.AspNetCore\":\"Warning\"}}"); // Configure http if (builder.Environment.IsDevelopment()) - Logger.Info( - "Disabling http pipeline configuration as the environment is set to development. All http endpoint config options in the core.json will be ignored"); +{ + preRunLogger.LogInformation("Disabling http pipeline configuration as the environment is set to development. All http endpoint config options in the core.json will be ignored"); +} else { var httpConfig = configService.Get().Http; @@ -49,16 +66,15 @@ else try { certificate = X509Certificate2.CreateFromPemFile(httpConfig.CertPath, httpConfig.KeyPath); - - Logger.Info($"Successfully loaded certificate '{certificate.FriendlyName}'"); + + preRunLogger.LogInformation("Successfully loaded certificate {name}", certificate.Subject); } catch (Exception e) { - Logger.Fatal("An error occured while loading certificate"); - Logger.Fatal(e); + preRunLogger.LogCritical("An error occured while loading certificate: {e}", e); } } - + builder.WebHost.ConfigureMoonCoreHttp( httpConfig.HttpPort, httpConfig.EnableSsl, @@ -68,35 +84,55 @@ else } // Build feature service and perform load -var featureService = new FeatureService(configService); +var featureService = new FeatureService( + configService, + preRunLoggerFactory.CreateLogger() +); + await featureService.Load(); // Build plugin service and perform load -var pluginService = new PluginService(); +var pluginService = new PluginService( + preRunLoggerFactory.CreateLogger() +); + await pluginService.Load(); try { // Check database migrations await DatabaseCheckHelper.Check( - new DataContext(configService), - false + preRunLoggerFactory.CreateLogger(), + new DataContext(configService) ); } catch (MySqlException e) { - if (e.InnerException is EndOfStreamException eosException) + bool IsBootException(MySqlException e) { - if (eosException.Message.Contains("read 4 header bytes")) + if (e.InnerException is EndOfStreamException eosException) { - Logger.Warn("The mysql server appears to be still booting up. Exiting..."); - - Environment.Exit(1); - return; + if (!eosException.Message.Contains("read 4 header bytes")) + return false; } + else if (e.InnerException is MySqlEndOfStreamException endOfStreamException) + { + if (!endOfStreamException.Message.Contains("An incomplete response was received from the server")) + return false; + } + else + throw e; + + return true; + } + + if (IsBootException(e)) + { + preRunLogger.LogWarning("The mysql server appears to be still booting up. Exiting..."); + + Environment.Exit(1); + return; } - - throw; } // Add pre constructed services @@ -105,7 +141,7 @@ builder.Services.AddSingleton(configService); builder.Services.AddSingleton(pluginService); // Feature hook -await featureService.PreInit(builder, pluginService); +await featureService.PreInit(builder, pluginService, preRunLoggerFactory); // Plugin hook await pluginService.PreInitialize(builder); diff --git a/Moonlight/_Imports.razor b/Moonlight/_Imports.razor index afa33440..ebad96cb 100644 --- a/Moonlight/_Imports.razor +++ b/Moonlight/_Imports.razor @@ -3,11 +3,23 @@ @using Microsoft.JSInterop @using Moonlight -@using MoonCoreUI.Components.Forms -@using MoonCoreUI.Components +@using MoonCore.Services +@using MoonCore.Helpers + +@using MoonCore.Blazor.Components +@using MoonCore.Blazor.Forms +@using MoonCore.Blazor.Services +@using MoonCore.Blazor.Helpers +@using MoonCore.Blazor.Forms.Table +@using MoonCore.Blazor.Forms.Auto +@using MoonCore.Blazor.Models.FastForms +@using MoonCore.Blazor.Models.FastForms.Validators +@using MoonCore.Blazor.Forms.FastForms +@using MoonCore.Blazor.Forms.FastForms.Components @using Moonlight.Core.UI @using Moonlight.Core.Attributes +@using Moonlight.Core.Extensions @using Moonlight.Core.UI.Components @using Moonlight.Core.UI.Components.Partials @using Moonlight.Core.UI.Components.Alerts \ No newline at end of file