Upgraded users, sessions and settings page

This commit is contained in:
Marcel Baumgartner
2024-06-25 17:52:58 +02:00
parent 7fcd674b7f
commit 2838a91e3c
6 changed files with 223 additions and 108 deletions

View File

@@ -78,9 +78,9 @@
<div class="d-flex justify-content-center">
<div class="d-flex align-items-center">
<div class="card card-body m-15 p-15">
<LazyLoader Load="Load">
<span></span>
</LazyLoader>
<LazyLoader Load="Load"
UseDefaultValues="false"
TimeUntilSpinnerIsShown="TimeSpan.Zero" />
</div>
</div>
</div>

View File

@@ -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)]
<AdminSysNavigation Index="1" />
<AdminSysNavigation Index="1"/>
@if (ModelToShow == null)
@if (CurrentModel == null)
{
<IconAlert Title="No resource to show" Icon="bx-x">
No model found to show. Please refresh the page to go back
@@ -20,20 +24,36 @@
}
else
{
<div class="mt-5">
<Tooltip>
Changes to these settings are live applied. The save button only make the changes persistently saved to disk
</Tooltip>
</div>
<div class="card mt-5 mb-5">
<div class="card-header border-bottom-0">
@{
string title;
if (Path.Length == 0)
title = "Configuration";
<h3 class="card-title">
@if (Path.Length == 0)
{
<span>Configuration</span>
}
else
{
title = "Configuration - " + string.Join(" - ", Path);
}
}
<span class="text-muted">
<span class="align-middle">Configuration</span>
<h3 class="card-title">@(title)</h3>
@foreach (var subPart in Path.SkipLast(1))
{
<i class="bx bx-sm bx-chevron-right me-1 align-middle"></i>
<span class="align-middle">@subPart</span>
}
</span>
<span>
<i class="bx bx-sm bx-chevron-right align-middle text-muted"></i>
<span class="align-middle">@Path.Last()</span>
</span>
}
</h3>
<div class="card-toolbar">
<WButton OnClick="Reload" CssClasses="btn btn-icon btn-warning me-3">
<i class="bx bx-sm bx-revision"></i>
@@ -45,33 +65,23 @@ else
</div>
</div>
<Tooltip>
Changes to these settings are live applied. The save button only make the changes persistently saved to disk
</Tooltip>
<div class="row mt-5">
<div class="col-md-3 col-12 mb-5">
<div class="card card-body">
@{
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)
{
<div class="d-flex flex-stack">
<div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/admin/sys/settings?section=@(Section + "/" + prop.Name)" class="fs-4 text-primary">@(prop.Name)</a>
<a href="/admin/sys/settings?section=@(Section + "/" + item)" class="fs-4 text-primary">
@item
</a>
</div>
</div>
}
@if (Path.Length != 0)
{
<div class="d-flex flex-stack @(props.Length != 0 ? "mt-5" : "")">
<div class="d-flex flex-stack @(SidebarItems.Length != 0 ? "mt-5" : "")">
<div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/admin/sys/@(GetBackPath())" class="fs-4 text-primary">Back</a>
</div>
@@ -80,42 +90,21 @@ else
</div>
</div>
<div class="col-md-9 col-12">
<div class="card card-body">
<div class="row g-5">
<LazyLoader @ref="LazyLoader" Load="Load">
<AutoFormGenerator Model="ModelToShow" />
@*
@foreach (var prop in Properties)
{
<div class="col-md-6 col-12">
@{
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
var rf = ComponentHelper.FromType(typeToCreate, parameters =>
{
parameters.Add("Data", ModelToShow);
parameters.Add("Property", prop);
});
}
@rf
</div>
}*@
<LazyLoader @ref="LazyLoader" Load="Load" UseDefaultValues="false" TimeUntilSpinnerIsShown="TimeSpan.Zero">
<FastForm Model="CurrentModel" OnConfigure="OnFormConfigure"/>
</LazyLoader>
</div>
</div>
</div>
</div>
}
@code
{
[Parameter]
[SupplyParameterFromQuery]
public string? Section { get; set; } = "";
[Parameter] [SupplyParameterFromQuery] public string? Section { get; set; } = "";
private object? ModelToShow;
private PropertyInfo[] Properties = Array.Empty<PropertyInfo>();
private string[] Path = Array.Empty<string>();
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<string>();
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<PropertyInfo>();
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);
@@ -147,16 +151,40 @@ else
await LazyLoader.Reload();
}
private void OnFormConfigure(FastConfiguration<object> 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}";
}
}
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<Func<object, object?>> 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<Func<object, object?>> lambda = Expression.Lambda<Func<object, object?>>(
castResult, param);
return lambda;
}
// From MoonCore. TODO: Maybe provide this and the above function as mooncore helper
private bool TryGetAttribute<T>(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;
}
}

View File

@@ -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 @@
<AdminUsersNavigation Index="0"/>
<AutoCrud TItem="User"
TCreateForm="CreateUserForm"
TUpdateForm="UpdateUserForm"
Loader="Load"
CustomAdd="Add"
ValidateUpdate="ValidateUpdate">
<FastCrud TItem="User"
Search="Search"
OnConfigure="OnConfigure"
OnConfigureCreate="OnConfigureCreate"
OnConfigureEdit="OnConfigureEdit">
<View>
<MCBColumn TItem="User" Field="@(x => x.Id)" Title="Id" Filterable="true"/>
<MCBColumn TItem="User" Field="@(x => x.Email)" Title="Email" Filterable="true"/>
<MCBColumn TItem="User" Field="@(x => x.Username)" Title="Username" Filterable="true"/>
<MCBColumn TItem="User" Field="@(x => x.CreatedAt)" Title="Created at"/>
</View>@*
<UpdateActions>
</View>
<EditToolbar>
<WButton OnClick="() => ChangePassword(context)" CssClasses="btn btn-info me-2">
<i class="bx bx-sm bxs-key"></i>
Change password
</WButton>
</UpdateActions>*@
</AutoCrud>
</EditToolbar>
</FastCrud>
@code
{
private IEnumerable<User> Load(Repository<User> 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))
@@ -53,17 +51,65 @@
});
}
private async Task Add(User user)
private void OnConfigure(FastCrudConfiguration<User> configuration)
{
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");
}
};
// To notify the authentication provider before we update the data in the database, we call it here
private async Task ValidateUpdate(User user)
configuration.ValidateEdit += async user =>
{
await AuthenticationProvider.ChangeDetails(user, user.Email, user.Username);
};
}
private void OnConfigureCreate(FastConfiguration<User> configuration)
{
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<string>(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short"))
.WithValidation<string>(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<string>(x => x.Length >= 8 ? ValidationResult.Success : new ValidationResult("The password must be at least 8 characters long"))
.WithValidation<string>(x => x.Length <= 256 ? ValidationResult.Success : new ValidationResult("The password must not be longer than 256 characters"));
}
private void OnConfigureEdit(FastConfiguration<User> 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<string>(x => x.Length >= 6 ? ValidationResult.Success : new ValidationResult("The username is too short"))
.WithValidation<string>(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<bool, SwitchComponent>()
.WithName("Two factor authentication")
.WithDescription("This toggles the use of the two factor authentication");
}
private IEnumerable<User> Search(IEnumerable<User> source, string term)
{
return source.Where(x => x.Username.Contains(term) || x.Email.Contains(term));
}
}

View File

@@ -62,8 +62,8 @@
<Template>
<div class="d-flex justify-content-end">
<div class="btn btn-group">
<WButton OnClick="() => Message(context)" Text="Message" CssClasses="btn btn-primary"/>
<WButton OnClick="() => Redirect(context)" Text="Redirect" CssClasses="btn btn-warning"/>
<WButton OnClick="() => Message(context)" CssClasses="btn btn-primary">Message</WButton>
<WButton OnClick="() => Redirect(context)" CssClasses="btn btn-warning">Redirect</WButton>
</div>
</div>
</Template>

View File

@@ -93,8 +93,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.3.8" />
<PackageReference Include="MoonCore.Blazor" Version="1.0.3" />
<PackageReference Include="MoonCore" Version="1.4.0" />
<PackageReference Include="MoonCore.Blazor" Version="1.0.8" />
<PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />

View File

@@ -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