diff --git a/Moonlight/Core/UI/Layouts/MainLayout.razor b/Moonlight/Core/UI/Layouts/MainLayout.razor index c7c93001..ab8dceb0 100644 --- a/Moonlight/Core/UI/Layouts/MainLayout.razor +++ b/Moonlight/Core/UI/Layouts/MainLayout.razor @@ -78,9 +78,9 @@
- - - +
diff --git a/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor b/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor index 12c2cee2..57000001 100644 --- a/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor +++ b/Moonlight/Core/UI/Views/Admin/Sys/Settings.razor @@ -1,7 +1,11 @@ @page "/admin/sys/settings" +@using System.ComponentModel +@using System.Linq.Expressions @using Moonlight.Core.UI.Components.Navigations @using System.Reflection +@using MoonCore.Blazor.Models.Fast +@using MoonCore.Helpers @using MoonCore.Services @using Moonlight.Core.Configuration @@ -10,9 +14,9 @@ @attribute [RequirePermission(9999)] - + -@if (ModelToShow == null) +@if (CurrentModel == null) { No model found to show. Please refresh the page to go back @@ -20,20 +24,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)

+
@@ -44,34 +64,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) { -
+
@@ -80,42 +90,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 +113,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") && + DefaultComponentSelector.GetDefault(x.PropertyType) != null // Check if a component has been registered for that type + ) + .ToArray(); } await InvokeAsync(StateHasChanged); @@ -146,16 +150,40 @@ else if (LazyLoader != null) await LazyLoader.Reload(); } + + private void OnFormConfigure(FastConfiguration 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); + else + propConfig.WithName(Formatter.ConvertCamelCaseToSpaces(property.Name)); + + 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 +205,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 2b9ad361..28aa6d6c 100644 --- a/Moonlight/Core/UI/Views/Admin/Users/Index.razor +++ b/Moonlight/Core/UI/Views/Admin/Users/Index.razor @@ -1,7 +1,11 @@ @page "/admin/users" +@using System.ComponentModel.DataAnnotations @using Moonlight.Core.UI.Components.Navigations @using MoonCore.Abstractions +@using MoonCore.Blazor.Forms.Fast.Components +@using MoonCore.Blazor.Models.Fast +@using MoonCore.Blazor.Models.Fast.Validators @using Moonlight.Core.Database.Entities @using MoonCore.Exceptions @using Moonlight.Core.Models.Abstractions @@ -14,36 +18,30 @@ - + - @* - + + Change password - *@ - + + @code { - private IEnumerable Load(Repository repository) - { - return repository.Get(); - } - private async Task ChangePassword(User user) { - await AlertService.Text("", "Enter a new password for {user.Username}", async newPassword => + 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)) @@ -52,18 +50,66 @@ await AuthenticationProvider.ChangePassword(user, newPassword); }); } - - private async Task Add(User user) + + private void OnConfigure(FastCrudConfiguration configuration) { - var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); + configuration.CustomCreate += async user => + { + var result = await AuthenticationProvider.Register(user.Username, user.Email, user.Password); - if (result == null) - throw new DisplayException("An unknown error occured while creating user"); + 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); + }; } - // 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 OnConfigureCreate(FastConfiguration configuration) { - await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username); + configuration.AddProperty(x => x.Username) + .WithDefaultComponent() + .WithValidation(FastValidators.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(FastValidators.Required) + .WithValidation(RegexValidator.Create("^.+@.+$", "You need to enter a valid email address")); + + configuration.AddProperty(x => x.Password) + .WithDefaultComponent() + .WithValidation(FastValidators.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")); + } + + private void OnConfigureEdit(FastConfiguration configuration, User currentUser) + { + configuration.AddProperty(x => x.Username) + .WithDefaultComponent() + .WithValidation(FastValidators.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(FastValidators.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 c4fac3f5..3f96fffe 100644 --- a/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor +++ b/Moonlight/Core/UI/Views/Admin/Users/Sessions.razor @@ -62,8 +62,8 @@ diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 50851cb9..58bb91c6 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -93,8 +93,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Moonlight/_Imports.razor b/Moonlight/_Imports.razor index f83e39b4..8704dafb 100644 --- a/Moonlight/_Imports.razor +++ b/Moonlight/_Imports.razor @@ -9,6 +9,7 @@ @using MoonCore.Blazor.Helpers @using MoonCore.Blazor.Forms.Table @using MoonCore.Blazor.Forms.Auto +@using MoonCore.Blazor.Forms.Fast @using Moonlight.Core.UI @using Moonlight.Core.Attributes