Upgraded users, sessions and settings page
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +16,7 @@
|
||||
|
||||
<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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user