Merge pull request #8 from leonhoppe/feature/generatedAdminPages

Feature/generated admin pages
This commit is contained in:
leonhoppe
2024-11-09 11:19:02 +01:00
committed by GitHub
64 changed files with 1923 additions and 1246 deletions

View File

@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -42,6 +44,10 @@ Global
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
EndGlobalSection EndGlobalSection

View File

@@ -1,4 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD; <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD; &lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD; &lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD;

View File

@@ -7,6 +7,14 @@ A simple backend management api for ASP.NET Core Web APIs
- [x] Permission management - [x] Permission management
- [x] Frontend dashboards - [x] Frontend dashboards
## 2.0 Todo list
- [x] 1.0 bug fixes
- [x] Code cleanup
- [x] Relations in database
- [x] Generated Admin pages
- [ ] Pretty Login page for administration
- [ ] Clean documentation
# Usage # Usage
There are two different versions of HopFrame, either the Web API version or the full Blazor web version. There are two different versions of HopFrame, either the Web API version or the full Blazor web version.

View File

@@ -17,6 +17,6 @@ public class PermissionGroup : IPermissionOwner {
[Required] [Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public virtual IList<Permission> Permissions { get; set; } public virtual List<Permission> Permissions { get; set; }
} }

View File

@@ -8,7 +8,7 @@ public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required, MinLength(36), MaxLength(36)]
public Guid Id { get; init; } public Guid Id { get; init; }
[MaxLength(50)] [Required, MaxLength(50)]
public string Username { get; set; } public string Username { get; set; }
[Required, MaxLength(50), EmailAddress] [Required, MaxLength(50), EmailAddress]
@@ -20,9 +20,9 @@ public class User : IPermissionOwner {
[Required] [Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public virtual IList<Permission> Permissions { get; set; } public virtual List<Permission> Permissions { get; set; }
[JsonIgnore] [JsonIgnore]
public virtual IList<Token> Tokens { get; set; } public virtual List<Token> Tokens { get; set; }
} }

View File

@@ -0,0 +1,9 @@
using HopFrame.Web.Admin.Generators;
namespace HopFrame.Web.Admin;
public abstract class AdminPagesContext {
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public sealed class AdminDescriptionAttribute(string description) : Attribute {
public string Description { get; set; } = description;
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public sealed class AdminNameAttribute(string name) : Attribute {
public string Name { get; set; } = name;
}

View File

@@ -0,0 +1,8 @@
namespace HopFrame.Web.Admin.Attributes.Classes;
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
public bool ShowCreateButton { get; set; } = showCreateButton;
public bool ShowDeleteButton { get; set; } = showDeleteButton;
public bool ShowUpdateButton { get; set; } = showUpdateButton;
}

View File

@@ -0,0 +1,13 @@
using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Attributes.Classes;
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
public AdminPagePermissions Permissions { get; set; } = new() {
Create = create,
Update = update,
Delete = delete,
View = view
};
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes.Classes;
[AttributeUsage(AttributeTargets.Class)]
public class AdminUrlAttribute(string url) : Attribute {
public string Url { get; set; } = url;
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public class AdminBoldAttribute(bool bold = true) : Attribute {
public bool Bold { get; set; } = bold;
}

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminHideValueAttribute : Attribute;

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
public bool OnlyForListing { get; set; } = onlyForListing;
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
public string Prefix { get; set; } = prefix;
}

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUneditableAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public class AdminUniqueAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUnsortableAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class ListingPropertyAttribute : Attribute;

View File

@@ -0,0 +1,12 @@
namespace HopFrame.Web.Admin.Generators;
public interface IAdminContextGenerator {
/// <summary>
/// Returns the generator object for the specified Admin Page. This needs to be within the same Admin Context.
/// </summary>
/// <typeparam name="TModel">The Model of the Admin Page</typeparam>
/// <returns></returns>
IAdminPageGenerator<TModel> Page<TModel>();
}

View File

@@ -0,0 +1,109 @@
using System.ComponentModel;
using System.Linq.Expressions;
namespace HopFrame.Web.Admin.Generators;
public interface IAdminPageGenerator<TModel> {
/// <summary>
/// Sets the title of the Admin Page
/// </summary>
/// <param name="title">the specified title</param>
/// <returns></returns>
IAdminPageGenerator<TModel> Title(string title);
/// <summary>
/// Sets the description of the Admin Page
/// </summary>
/// <param name="description">the specified description</param>
/// <returns></returns>
IAdminPageGenerator<TModel> Description(string description);
/// <summary>
/// Sets the url for the Admin Page
/// </summary>
/// <param name="url">the specified url (administration/{url})</param>
/// <returns></returns>
IAdminPageGenerator<TModel> Url(string url);
/// <summary>
/// Sets the permission needed to view the Admin Page
/// </summary>
/// <param name="permission">the specified permission</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ViewPermission(string permission);
/// <summary>
/// Sets the permission needed to create a new Entry
/// </summary>
/// <param name="permission">the specified permission</param>
/// <returns></returns>
IAdminPageGenerator<TModel> CreatePermission(string permission);
/// <summary>
/// Sets the permission needed to update an Entry
/// </summary>
/// <param name="permission">the specified permission</param>
/// <returns></returns>
IAdminPageGenerator<TModel> UpdatePermission(string permission);
/// <summary>
/// Sets the permission needed to delete an Entry
/// </summary>
/// <param name="permission">the specified permission</param>
/// <returns></returns>
IAdminPageGenerator<TModel> DeletePermission(string permission);
/// <summary>
/// Enables or disables the create button
/// </summary>
/// <param name="show">the specified state</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ShowCreateButton(bool show);
/// <summary>
/// Enables or disables the delete button
/// </summary>
/// <param name="show">the specified state</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ShowDeleteButton(bool show);
/// <summary>
/// Enables or disables the update button
/// </summary>
/// <param name="show">the specified state</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ShowUpdateButton(bool show);
/// <summary>
/// Specifies the default sort property and direction
/// </summary>
/// <param name="propertyExpression">Which property should be sorted</param>
/// <param name="direction">In which direction should be sorted</param>
/// <returns></returns>
IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction);
/// <summary>
/// Specifies the repository for the page
/// </summary>
/// <typeparam name="TRepository">The specified repository</typeparam>
/// <returns></returns>
IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel>;
/// <summary>
/// Returns the generator of the specified property
/// </summary>
/// <param name="propertyExpression">The property</param>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
/// <summary>
/// Specifies the default property that should be displayed as a property in other listings
/// </summary>
/// <param name="propertyExpression">The property</param>
/// <returns></returns>
IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
}

View File

@@ -0,0 +1,123 @@
using System.Linq.Expressions;
namespace HopFrame.Web.Admin.Generators;
public interface IAdminPropertyGenerator<TProperty, TModel> {
/// <summary>
/// Should the property be sortable or not
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable);
/// <summary>
/// Should the admin be able to edit the property after creation or not
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable);
/// <summary>
/// Should the value of the property be displayed while editing or not (useful for passwords and tokens)
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display);
/// <summary>
/// Should the property be a column on the page list or not
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true);
/// <summary>
/// Should the property be ignored completely
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = true);
/// <summary>
/// Is the value of the property database generated and is not meant to be changed
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true);
/// <summary>
/// Should the property value be bold in the listing or not
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true);
/// <summary>
/// Is the value of the property unique under all other entries in the dataset
/// </summary>
/// <param name="unique"></param>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true);
/// <summary>
/// Specifies the display name in the listing and editing/creation
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName);
/// <summary>
/// Has the value of the property a never changing prefix that doesn't need to be specified or displayed
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix);
/// <summary>
/// The specified function gets called before creation/edit to verify that the entered value matches the property requirements
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator);
/// <summary>
/// Sets the input type in creation/edit to a selector for the property type. The property type needs to have its own admin page in order for the selector to work!
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true);
/// <summary>
/// Sets the input type in creation/edit to a selector for the specified type. The specified type needs to have its own admin page in order for the selector to work!
/// </summary>
/// <param name="selector"></param>
/// <typeparam name="TSelectorType"></typeparam>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true);
/// <summary>
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
/// </summary>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser);
/// <summary>
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
/// </summary>
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser);
/// <summary>
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
/// </summary>
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser);
/// <summary>
/// Specifies the default property that should be displayed as a value
/// </summary>
/// <param name="propertyExpression"></param>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression);
/// <summary>
/// Specifies the default property that should be displayed as a value
/// </summary>
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
/// <returns></returns>
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression);
}

View File

@@ -0,0 +1,11 @@
namespace HopFrame.Web.Admin.Generators;
public interface IGenerator<out TGeneratedType> {
/// <summary>
/// Compiles the generator with all specified options
/// </summary>
/// <returns>The compiled data structure</returns>
TGeneratedType Compile();
}

View File

@@ -0,0 +1,79 @@
using HopFrame.Web.Admin.Models;
using HopFrame.Web.Admin.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Web.Admin.Generators.Implementation;
internal class AdminContextGenerator : IAdminContextGenerator {
private readonly IDictionary<Type, object> _adminPages = new Dictionary<Type, object>();
public IAdminPageGenerator<TModel> Page<TModel>() {
if (_adminPages.TryGetValue(typeof(TModel), out var pageGenerator))
return pageGenerator as IAdminPageGenerator<TModel>;
var generator = Activator.CreateInstance(typeof(IAdminPageGenerator<TModel>)) as AdminPageGenerator<TModel>;
generator?.ApplyConfigurationFromAttributes(typeof(TModel).GetCustomAttributes(false));
_adminPages.Add(typeof(TModel), generator);
return generator;
}
public AdminPage<TModel> CompilePage<TModel>() {
var generator = _adminPages[typeof(TModel)];
if (generator is null) return null;
return (generator as AdminPageGenerator<TModel>)?.Compile();
}
public TContext CompileContext<TContext>() where TContext : AdminPagesContext {
var type = typeof(TContext);
var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage));
var properties = type.GetProperties();
var context = Activator.CreateInstance<TContext>();
foreach (var property in properties) {
var propertyType = property.PropertyType.GenericTypeArguments[0];
var pageGeneratorType = typeof(AdminPageGenerator<>).MakeGenericType(propertyType);
var generatorInstance = Activator.CreateInstance(pageGeneratorType);
var titleMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.Title));
titleMethod?.Invoke(generatorInstance, [property.Name]);
var populateMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.ApplyConfigurationFromAttributes));
populateMethod?.Invoke(generatorInstance, [propertyType.GetCustomAttributes(false)]);
_adminPages.Add(propertyType, generatorInstance);
}
context.OnModelCreating(this);
foreach (var property in properties) {
var modelType = property.PropertyType.GenericTypeArguments[0];
var method = compileMethod?.MakeGenericMethod(modelType);
property.SetValue(context, method?.Invoke(this, []));
}
return context;
}
public static void RegisterPages(AdminPagesContext context, IAdminPagesProvider provider, IServiceCollection services) {
var properties = context.GetType().GetProperties();
foreach (var property in properties) {
var page = property.GetValue(context) as AdminPage;
if (page is null) continue;
provider.RegisterAdminPage(page.Title.ToLower(), page);
if (page.RepositoryProvider is not null)
services.AddScoped(page.RepositoryProvider);
}
}
}

View File

@@ -0,0 +1,190 @@
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Attributes.Classes;
using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Generators.Implementation;
internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>, IGenerator<AdminPage<TModel>> {
public readonly AdminPage<TModel> Page;
private readonly Dictionary<string, object> _propertyGenerators;
public AdminPageGenerator() {
Page = new AdminPage<TModel> {
Permissions = new AdminPagePermissions(),
ModelType = typeof(TModel)
};
_propertyGenerators = new Dictionary<string, object>();
var type = typeof(TModel);
var properties = type.GetProperties();
var generatorType = typeof(AdminPropertyGenerator<,>);
foreach (var property in properties) {
var attributes = property.GetCustomAttributes(false);
var genericType = generatorType.MakeGenericType(property.PropertyType, type);
var generator = Activator.CreateInstance(genericType, [property.Name, property.PropertyType]);
var method = genericType
.GetMethod(nameof(AdminPropertyGenerator<object, object>.ApplyConfigurationFromAttributes))?
.MakeGenericMethod(type);
method?.Invoke(generator, [this, attributes, property]);
_propertyGenerators.Add(property.Name, generator);
}
}
public IAdminPageGenerator<TModel> Title(string title) {
Page.Title = title;
Page.Url ??= title.ToLower();
return this;
}
public IAdminPageGenerator<TModel> Description(string description) {
Page.Description = description;
return this;
}
public IAdminPageGenerator<TModel> Url(string url) {
Page.Url = url;
return this;
}
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
Page.Permissions.View = permission;
return this;
}
public IAdminPageGenerator<TModel> CreatePermission(string permission) {
Page.Permissions.Create = permission;
return this;
}
public IAdminPageGenerator<TModel> UpdatePermission(string permission) {
Page.Permissions.Update = permission;
return this;
}
public IAdminPageGenerator<TModel> DeletePermission(string permission) {
Page.Permissions.Delete = permission;
return this;
}
public IAdminPageGenerator<TModel> ShowCreateButton(bool show) {
Page.ShowCreateButton = show;
return this;
}
public IAdminPageGenerator<TModel> ShowDeleteButton(bool show) {
Page.ShowDeleteButton = show;
return this;
}
public IAdminPageGenerator<TModel> ShowUpdateButton(bool show) {
Page.ShowUpdateButton = show;
return this;
}
public IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction) {
var property = GetPropertyInfo(propertyExpression);
Page.DefaultSortPropertyName = property.Name;
Page.DefaultSortDirection = direction;
return this;
}
public IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel> {
Page.RepositoryProvider = typeof(TRepository);
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
var property = GetPropertyInfo(propertyExpression);
if (_propertyGenerators.TryGetValue(property.Name, out var propertyGenerator))
return propertyGenerator as AdminPropertyGenerator<TProperty, TModel>;
var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator<TProperty, TModel>), new { property.Name, property.PropertyType }) as AdminPropertyGenerator<TProperty, TModel>;
generator?.ApplyConfigurationFromAttributes(this, property.GetCustomAttributes(false), property);
_propertyGenerators.Add(property.Name, generator);
return generator;
}
public IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
var property = GetPropertyInfo(propertyExpression);
Page.ListingProperty = property.Name;
return this;
}
public AdminPage<TModel> Compile() {
var properties = new List<AdminPageProperty>();
foreach (var generator in _propertyGenerators.Values) {
var method = generator.GetType().GetMethod(nameof(AdminPropertyGenerator<object, object>.Compile));
var prop = method?.Invoke(generator, []) as AdminPageProperty;
properties.Add(prop);
}
Page.Properties = properties;
return Page;
}
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
if (propertyLambda.Body is not MemberExpression member) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
}
if (member.Member is not PropertyInfo propInfo) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
}
Type type = typeof(TSource);
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType)) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
}
if (propInfo.Name is null)
throw new ArgumentException($"Expression '{propertyLambda}' refers a not existing property.");
return propInfo;
}
public void ApplyConfigurationFromAttributes(object[] attributes) {
if (attributes.Any(a => a is AdminNameAttribute)) {
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
Title(attribute?.Name);
}
if (attributes.Any(a => a is AdminDescriptionAttribute)) {
var attribute = attributes.Single(a => a is AdminDescriptionAttribute) as AdminDescriptionAttribute;
Description(attribute?.Description);
}
if (attributes.Any(a => a is AdminUrlAttribute)) {
var attribute = attributes.Single(a => a is AdminUrlAttribute) as AdminUrlAttribute;
Url(attribute?.Url);
}
if (attributes.Any(a => a is AdminPermissionsAttribute)) {
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
CreatePermission(attribute?.Permissions.Create);
UpdatePermission(attribute?.Permissions.Update);
ViewPermission(attribute?.Permissions.View);
DeletePermission(attribute?.Permissions.Delete);
}
if (attributes.Any(a => a is AdminButtonConfigAttribute)) {
var attribute = attributes.Single(a => a is AdminButtonConfigAttribute) as AdminButtonConfigAttribute;
ShowCreateButton(attribute?.ShowCreateButton == true);
ShowUpdateButton(attribute?.ShowUpdateButton == true);
ShowDeleteButton(attribute?.ShowDeleteButton == true);
}
}
}

View File

@@ -0,0 +1,170 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
using System.Reflection;
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Attributes.Members;
using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Generators.Implementation;
internal sealed class AdminPropertyGenerator<TProperty, TModel>(string name, Type type) : IAdminPropertyGenerator<TProperty, TModel>, IGenerator<AdminPageProperty> {
private readonly AdminPageProperty _property = new() {
Name = name,
Type = type
};
public IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable) {
_property.Sortable = sortable;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable) {
_property.Editable = editable;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display) {
_property.EditDisplayValue = display;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true) {
_property.DisplayInListing = display;
_property.Sortable = false;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = false) {
_property.Ignore = ignore;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true) {
_property.Generated = generated;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true) {
_property.Bold = bold;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true) {
_property.Unique = unique;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName) {
_property.DisplayName = displayName;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix) {
_property.Prefix = prefix;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator) {
_property.Validator = o => validator.Invoke((TProperty)o);
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true) {
_property.Selector = selector;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true) {
_property.Selector = true;
_property.SelectorType = typeof(TSelectorType);
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString());
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression) {
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
_property.DisplayPropertyName = property.Name;
return this;
}
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression) {
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
_property.DisplayPropertyName = property.Name;
return this;
}
public AdminPageProperty Compile() {
_property.DisplayName ??= _property.Name;
return _property;
}
public void ApplyConfigurationFromAttributes<T>(AdminPageGenerator<T> pageGenerator, object[] attributes, PropertyInfo property) {
if (attributes.Any(a => a is KeyAttribute)) {
pageGenerator.Page.DefaultSortPropertyName = property.Name;
Editable(false);
Bold();
}
if (attributes.Any(a => a is AdminUnsortableAttribute))
Sortable(false);
if (attributes.Any(a => a is AdminUneditableAttribute))
Editable(false);
if (attributes.Any(a => a is AdminUniqueAttribute))
Unique();
if (attributes.Any(a => a is AdminIgnoreAttribute)) {
var attribute = attributes.Single(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute;
DisplayInListing(false);
Sortable(false);
Ignore(attribute?.OnlyForListing == false);
}
if (attributes.Any(a => a is AdminHideValueAttribute))
DisplayValueWhileEditing(false);
if (attributes.Any(a => a is DatabaseGeneratedAttribute))
Generated();
if (attributes.Any(a => a is AdminNameAttribute)) {
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
DisplayName(attribute?.Name);
}
if (attributes.Any(a => a is AdminBoldAttribute)) {
var attribute = attributes.Single(a => a is AdminBoldAttribute) as AdminBoldAttribute;
Bold(attribute?.Bold == true);
}
if (attributes.Any(a => a is RequiredAttribute)) {
_property.Required = true;
}
if (attributes.Any(a => a is AdminPrefixAttribute)) {
var attribute = attributes.Single(a => a is AdminPrefixAttribute) as AdminPrefixAttribute;
Prefix(attribute?.Prefix);
}
if (attributes.Any(a => a is ListingPropertyAttribute)) {
var attribute = attributes.Single(a => a is ListingPropertyAttribute) as ListingPropertyAttribute;
_property.DisplayPropertyName = property.Name;
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
namespace HopFrame.Web.Admin;
public abstract class ModelRepository<TModel> : IModelRepository {
public abstract Task<IEnumerable<TModel>> ReadAll();
public abstract Task<TModel> Create(TModel model);
public abstract Task<TModel> Update(TModel model);
public abstract Task Delete(TModel model);
public async Task<IEnumerable<object>> ReadAllO() {
var models = await ReadAll();
return models.Select(m => (object)m);
}
public async Task<object> CreateO(object model) {
return await Create((TModel)model);
}
public async Task<object> UpdateO(object model) {
return await Update((TModel)model);
}
public Task DeleteO(object model) {
return Delete((TModel)model);
}
}
public interface IModelRepository {
Task<IEnumerable<object>> ReadAllO();
Task<object> CreateO(object model);
Task<object> UpdateO(object model);
Task DeleteO(object model);
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace HopFrame.Web.Admin.Models;
public sealed class AdminPage<TModel> : AdminPage;
public class AdminPage {
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public AdminPagePermissions Permissions { get; set; }
public IList<AdminPageProperty> Properties { get; set; }
public string ListingProperty { get; set; }
public Type RepositoryProvider { get; set; }
public Type ModelType { get; set; }
public string DefaultSortPropertyName { get; set; }
public ListSortDirection DefaultSortDirection { get; set; }
public bool ShowCreateButton { get; set; } = true;
public bool ShowDeleteButton { get; set; } = true;
public bool ShowUpdateButton { get; set; } = true;
}

View File

@@ -0,0 +1,8 @@
namespace HopFrame.Web.Admin.Models;
public sealed class AdminPagePermissions {
public string View { get; set; }
public string Create { get; set; }
public string Update { get; set; }
public string Delete { get; set; }
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace HopFrame.Web.Admin.Models;
public sealed class AdminPageProperty {
public string Name { get; set; }
public string DisplayName { get; set; }
public string Prefix { get; set; }
public string DisplayPropertyName { get; set; }
public bool DisplayInListing { get; set; } = true;
public bool Sortable { get; set; } = true;
public bool Editable { get; set; } = true;
public bool EditDisplayValue { get; set; } = true;
public bool Generated { get; set; }
public bool Bold { get; set; }
public bool Required { get; set; }
public bool Ignore { get; set; }
public bool Unique { get; set; }
public bool Selector { get; set; }
public Type SelectorType { get; set; }
public Type Type { get; set; }
public Func<object, string> Validator { get; set; }
public Func<object, object, object> Parser { get; set; }
public object GetValue(object entry) {
return entry.GetType().GetProperty(Name)?.GetValue(entry);
}
public T GetValue<T>(object entry) {
return (T)entry.GetType().GetProperty(Name)?.GetValue(entry);
}
public void SetValue(object entry, object value) {
entry.GetType().GetProperty(Name)?.SetValue(entry, value);
}
}

View File

@@ -0,0 +1,12 @@
using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Providers;
public interface IAdminPagesProvider {
internal void RegisterAdminPage(string url, AdminPage page);
AdminPage LoadAdminPage(string url);
IList<AdminPage> LoadRegisteredAdminPages();
AdminPage HasPageFor(Type type);
}

View File

@@ -0,0 +1,26 @@
using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Providers.Implementation;
public class AdminPagesProvider : IAdminPagesProvider {
private readonly IDictionary<string, AdminPage> _pages = new Dictionary<string, AdminPage>();
public void RegisterAdminPage(string url, AdminPage page) {
_pages.Add(url, page);
}
public AdminPage LoadAdminPage(string url) {
return _pages.TryGetValue(url, out var page) ? page : null;
}
public IList<AdminPage> LoadRegisteredAdminPages() {
return _pages.Values.ToList();
}
public AdminPage HasPageFor(Type type) {
return _pages
.Where(p => p.Value.ModelType == type)
.Select(p => p.Value)
.SingleOrDefault();
}
}

View File

@@ -0,0 +1,29 @@
using HopFrame.Web.Admin.Generators.Implementation;
using HopFrame.Web.Admin.Providers;
using HopFrame.Web.Admin.Providers.Implementation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace HopFrame.Web.Admin;
public static class ServiceCollectionExtensions {
private static IAdminPagesProvider _provider;
public static IServiceCollection AddAdminContext<TContext>(this IServiceCollection services) where TContext : AdminPagesContext {
var provider = GetProvider();
services.TryAddSingleton(provider);
var generator = new AdminContextGenerator();
var context = generator.CompileContext<TContext>();
AdminContextGenerator.RegisterPages(context, provider, services);
services.AddSingleton(context);
return services;
}
private static IAdminPagesProvider GetProvider() {
return _provider ??= new AdminPagesProvider();
}
}

View File

@@ -0,0 +1,373 @@
@rendermode InteractiveServer
@using System.Collections
@using System.Globalization
@using BlazorStrap
@using BlazorStrap.Shared.Components.Modal
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Admin
@using HopFrame.Web.Admin.Models
@using HopFrame.Web.Admin.Providers
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web
<BSModal DataId="admin-page-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
<BSForm TValue="object" EditContext="_context" OnValidSubmit="Save">
@if (!_isEdit) {
<BSModalHeader>Create entry</BSModalHeader>
}
else {
<BSModalHeader>Edit entry</BSModalHeader>
}
<BSModalContent>
@foreach (var prop in GetEditableProperties()) {
@if (!_isEdit && prop.Generated) continue;
<div class="mb-3">
@if (IsListType(prop)) {
<BSLabel>@prop.DisplayName</BSLabel>
<BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var element in GetListPropertyValues(prop).Select((e, i) => new { e, i })) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => DeleteListItem(prop, element.i)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@element.e</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div>
@if (!prop.Selector) {
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
<input type="text" class="form-control" @onchange="v => _inputValues[prop] = (string)v.Value" required/>
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
</form>
}
else {
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
<select class="form-select" @onchange="e => _inputValues[prop] = ReadSelectorValue(prop, e.Value)">
<option selected>Select</option>
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
<option value="@element.Item2">@element.Item1</option>
}
</select>
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
</form>
}
</div>
</BSListGroupItem>
</BSListGroup>
}
else if (IsSwitch(prop)) {
<div class="form-check form-switch">
<BSLabel>@prop.DisplayName</BSLabel>
<input class="form-check-input" type="checkbox" checked="@_values[prop]" @onchange="e => _values[prop] = Convert.ToBoolean(e.Value)">
</div>
}
else if (prop.Prefix is not null && !_isEdit) {
<BSInputGroup>
<span class="@BS.Input_Group_Text">@prop.Prefix</span>
<input type="text" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = prop.Prefix + e.Value"/>
</BSInputGroup>
}
else if (prop.Selector) {
<BSLabel>@prop.DisplayName</BSLabel>
<select class="form-select" @onchange="e => _values[prop] = ReadSelectorValue(prop, e.Value)">
<option>Select</option>
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
<option value="@element.Item2" selected="@IsIndexSelected(prop, element.Item2)">@element.Item1</option>
}
</select>
}
else {
<BSLabel>@prop.DisplayName</BSLabel>
<input type="@GetInputType(prop)" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/>
@if (_validation[_validationIdentifiers[prop]].Any()) {
<div class="invalid-feedback" style="display: block">
@_validation[_validationIdentifiers[prop]].First()
</div>
}
}
</div>
}
</BSModalContent>
<BSModalFooter>
<BSButton Target="admin-page-modal">Cancel</BSButton>
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
</BSModalFooter>
</BSForm>
</BSModal>
@inject IServiceProvider Provider
@inject IAdminPagesProvider PageProvider
@inject SweetAlertService Alerts
@inject IPermissionRepository Permissions
@inject ITokenContext Auth
@code {
#pragma warning disable CS4014
[Parameter]
public Func<Task> ReloadDelegate { get; set; }
private BSModalBase _modal;
private EditContext _context;
private ValidationMessageStore _validation;
private Dictionary<AdminPageProperty, FieldIdentifier> _validationIdentifiers;
private IDictionary<AdminPageProperty, object> _values;
private Dictionary<AdminPageProperty, object[]> _selectorValues;
private IModelRepository _repository;
private AdminPage _currentPage;
private object _entry;
private bool _isEdit;
private IDictionary<AdminPageProperty, object> _inputValues;
public async Task Show(AdminPage page, object entryToEdit = null) {
_entry = null;
_inputValues = new Dictionary<AdminPageProperty, object>();
_selectorValues = new Dictionary<AdminPageProperty, object[]>();
_currentPage = page;
_entry = entryToEdit;
_isEdit = entryToEdit is not null;
_repository = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository;
_entry ??= Activator.CreateInstance(_currentPage.ModelType);
_context = new EditContext(_entry);
_validation = new ValidationMessageStore(_context);
_validationIdentifiers = new Dictionary<AdminPageProperty, FieldIdentifier>();
_context.OnValidationRequested += Validate;
_values = new Dictionary<AdminPageProperty, object>();
foreach (var property in _currentPage.Properties) {
_values.Add(property, property.GetValue(_entry));
_validationIdentifiers.Add(property, new FieldIdentifier(_entry, property.Name));
}
await _modal.ShowAsync();
}
private IList<AdminPageProperty> GetEditableProperties() {
return _currentPage.Properties
.Where(p => !p.Ignore)
.OrderBy(p => p.Editable)
.ToList();
}
private bool IsDisabled(AdminPageProperty prop) => (_isEdit && !prop.Editable) || prop.Generated;
private bool IsRequired(AdminPageProperty prop) => !_isEdit ? prop.Required : prop.Required && prop.EditDisplayValue;
private bool IsSwitch(AdminPageProperty prop) => prop.Type == typeof(bool);
private bool IsListType(AdminPageProperty prop) => IsListType(prop.Type);
private bool IsListType(Type type) {
if (!type.IsGenericType) return false;
var generic = type.GenericTypeArguments[0];
var gListType = typeof(IList<>).MakeGenericType(generic);
var iListType = typeof(List<>).MakeGenericType(generic);
return type.IsAssignableFrom(gListType) || type.IsAssignableFrom(iListType);
}
private IList<string> GetListPropertyValues(AdminPageProperty prop) {
if (!IsListType(prop)) return new List<string>();
var list = new List<string>();
var values = prop.GetValue(_entry);
if (values is null) {
prop.SetValue(_entry, Activator.CreateInstance(prop.Type));
return list;
}
foreach (var value in (IEnumerable)values) {
list.Add(MapPropertyValue(value, prop));
}
return list;
}
private string GetPropertyValue(AdminPageProperty property) {
if (!_isEdit) return "";
if (!property.EditDisplayValue) return "";
return MapPropertyValue(property.GetValue(_entry), property);
}
public string MapPropertyValue(object value, AdminPageProperty property, bool isSubProperty = false) {
if (value is null) return string.Empty;
var type = value.GetType();
var page = PageProvider.HasPageFor(type);
if (page is not null && !string.IsNullOrWhiteSpace(page.ListingProperty)) {
var prop = page.Properties
.SingleOrDefault(p => p.Name == page.ListingProperty);
if (prop is not null) {
return MapPropertyValue(prop.GetValue(value), prop);
}
}
if (!string.IsNullOrEmpty(property.DisplayPropertyName) && !isSubProperty) {
var prop = type.GetProperties()
.SingleOrDefault(p => p.Name == property.DisplayPropertyName);
return MapPropertyValue(prop?.GetValue(value), property, true);
}
var stringValue = value.ToString();
if (!string.IsNullOrWhiteSpace(property.Prefix)) {
return stringValue?.Replace(property.Prefix, "");
}
return stringValue;
}
private string GetInputType(AdminPageProperty property) {
if (!property.EditDisplayValue)
return "password";
return "text";
}
private void Validate(object sender, ValidationRequestedEventArgs e) {
_validation.Clear();
foreach (var value in _values) {
if (value.Key.Unique) {
if (value.Value == value.Key.GetValue(_entry)) continue;
var repo = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository;
var data = repo!.ReadAllO().GetAwaiter().GetResult();
foreach (var entry in data) {
var other = value.Key.GetValue(entry);
if (!other.Equals(value.Value)) continue;
_validation.Add(_validationIdentifiers[value.Key], $"This {value.Key.DisplayName ?? value.Key.Name} already exists!");
break;
}
}
if (value.Key.Validator is null) continue;
var error = value.Key.Validator?.Invoke(value.Value);
if (string.IsNullOrEmpty(error)) continue;
_validation.Add(_validationIdentifiers[value.Key], error);
}
}
private void DeleteListItem(AdminPageProperty prop, int index) {
var list = prop.GetValue<IList>(_entry);
list.RemoveAt(index);
}
private void AddListItem(AdminPageProperty prop) {
if (!_inputValues.TryGetValue(prop, out var input) || input is null) {
Alerts.FireAsync(new SweetAlertOptions {
Title = "Error!",
Text = "Please enter a value!",
Icon = SweetAlertIcon.Error
});
return;
}
var list = prop.GetValue<IList>(_entry);
var value = prop.Parser?.Invoke(_entry, input) ?? input;
list?.Add(value);
}
private async Task<(string, int)[]> SetupSelectorProperty(AdminPageProperty property) {
var type = property.SelectorType ?? property.Type;
if (IsListType(type)) {
type = type.GenericTypeArguments[0];
}
var page = PageProvider.HasPageFor(type);
if (page is null) {
throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!");
}
var repo = Provider.GetService(page.RepositoryProvider) as IModelRepository;
var objects = (await repo!.ReadAllO()).ToArray();
_selectorValues[property] = objects;
var data = new List<(string, int)>();
for (var i = 0; i < objects.Length; i++) {
data.Add((MapPropertyValue(objects[i], property), i));
}
return data.ToArray();
}
private bool IsIndexSelected(AdminPageProperty property, int index) {
var value = property.GetValue(_entry);
if (value is null) return false;
return _selectorValues[property][index] == value;
}
private object ReadSelectorValue(AdminPageProperty property, object value) {
if (!int.TryParse(value.ToString(), out int result)) {
return null;
}
return _selectorValues[property][result];
}
private async void Save() {
if (_isEdit && _currentPage.Permissions.Update is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to edit an entry!",
Icon = SweetAlertIcon.Error
});
return;
}
}else if (_currentPage.Permissions.Create is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to add an entry!",
Icon = SweetAlertIcon.Error
});
return;
}
}
foreach (var value in _values) {
if (IsListType(value.Key)) continue;
value.Key.SetValue(_entry, value.Key.Parser?.Invoke(_entry, value.Value) ?? Convert.ChangeType(value.Value, value.Key.Type));
}
if (!_isEdit) {
await _repository.CreateO(_entry);
Alerts.FireAsync(new SweetAlertOptions {
Title = "New entry added!",
Icon = SweetAlertIcon.Success,
ShowConfirmButton = false,
Timer = 1500
});
}
else {
await _repository.UpdateO(_entry);
Alerts.FireAsync(new SweetAlertOptions {
Title = "Entry updated!",
Icon = SweetAlertIcon.Success,
ShowConfirmButton = false,
Timer = 1500
});
}
await ReloadDelegate.Invoke();
}
}

View File

@@ -1,292 +0,0 @@
@rendermode InteractiveServer
@using BlazorStrap
@using BlazorStrap.Shared.Components.Modal
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Model
<BSModal DataId="add-group-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
<BSForm Model="_group" OnValidSubmit="AddGroup">
@if (_isEdit) {
<BSModalHeader>Edit group</BSModalHeader>
}
else {
<BSModalHeader>Add group</BSModalHeader>
}
<BSModalContent>
<div class="mb-3">
<BSLabel>Name</BSLabel>
@if (!_isEdit) {
<BSInputGroup>
<span class="@BS.Input_Group_Text">group.</span>
<BSInput InputType="InputType.Text" @bind-Value="_group.GroupName" required/>
</BSInputGroup>
}
else {
<input type="text" class="form-control" disabled value="@_group.Name"/>
}
</div>
@if (_isEdit) {
<div class="mb-3">
<BSLabel>Created at</BSLabel>
<input type="text" class="form-control" disabled value="@_group.CreatedAt"/>
</div>
}
<div class="mb-3">
<BSLabel>Description</BSLabel>
<BSInput InputType="InputType.TextArea" @bind-Value="_group.Description"/>
</div>
<div class="mb-3">
<BSInputSwitch @bind-Value="_group.IsDefaultGroup" CheckedValue="true" UnCheckedValue="false">
Default group
</BSInputSwitch>
</div>
<div class="mb-3">
<BSLabel>Inherits from</BSLabel>
<BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var group in _group.Permissions.Where(g => g.PermissionName.StartsWith("group."))) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(group)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@group.PermissionName.Replace("group.", "")</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div style="display: flex; gap: 20px">
<BSInput InputType="InputType.Select" @bind-Value="_groupToAdd">
<option selected>Select group</option>
@foreach (var group in _allGroups) {
@if (_group.Permissions.All(g => g.PermissionName != group.Name) && group.Name != _group.Name) {
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
}
}
</BSInput>
<BSButton Color="BSColor.Secondary" OnClick="AddInheritanceGroup">Add</BSButton>
</div>
</BSListGroupItem>
</BSListGroup>
</div>
<div class="mb-3">
<BSLabel>Permissions</BSLabel>
<BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var perm in _group.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@perm.PermissionName</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div style="display: flex; gap: 20px">
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
</div>
</BSListGroupItem>
</BSListGroup>
</div>
</BSModalContent>
<BSModalFooter>
<BSButton Target="add-group-modal">Cancel</BSButton>
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
</BSModalFooter>
</BSForm>
</BSModal>
@inject IGroupRepository Groups
@inject IPermissionRepository Permissions
@inject SweetAlertService Alerts
@inject ITokenContext Context
@code {
[Parameter] public Func<Task> ReloadPage { get; set; }
private PermissionGroupAdd _group;
private BSModalBase _modal;
private string _permissionToAdd;
private string _groupToAdd;
private IList<PermissionGroup> _allGroups;
private bool _isEdit;
public async Task ShowAsync(PermissionGroup group = null) {
_allGroups = await Groups.GetPermissionGroups();
if (group is not null) {
_group = new PermissionGroupAdd {
CreatedAt = group.CreatedAt,
Description = group.Description,
Name = group.Name,
IsDefaultGroup = group.IsDefaultGroup,
Permissions = group.Permissions
};
_isEdit = true;
}
else {
_group = new PermissionGroupAdd {
Permissions = new List<Permission>(),
IsDefaultGroup = false
};
_isEdit = false;
}
await _modal.ShowAsync();
}
private async Task AddPermission() {
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Enter a permission name!",
Icon = SweetAlertIcon.Error,
ShowConfirmButton = true
});
return;
}
if (_isEdit) {
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions();
return;
}
await Permissions.AddPermission(_group, _permissionToAdd);
}
_group.Permissions.Add(new Permission {
PermissionName = _permissionToAdd,
GrantedAt = DateTime.Now
});
_permissionToAdd = null;
}
private async Task RemovePermission(Permission permission) {
if (_isEdit) {
await Permissions.RemovePermission(_group, permission.PermissionName);
}
_group.Permissions.Remove(permission);
}
private async Task AddInheritanceGroup() {
if (string.IsNullOrWhiteSpace(_groupToAdd)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Select a group!",
Icon = SweetAlertIcon.Error,
ShowConfirmButton = true
});
return;
}
if (_isEdit) {
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions();
return;
}
await Permissions.AddPermission(_group, _groupToAdd);
}
_group.Permissions.Add(new Permission {
PermissionName = _groupToAdd
});
_groupToAdd = null;
}
private async Task AddGroup() {
if (_isEdit) {
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions();
return;
}
await Groups.EditPermissionGroup(_group);
if (ReloadPage is not null)
await ReloadPage.Invoke();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Group edited!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
return;
}
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.AddGroup)) {
await NoAddPermissions();
return;
}
if (_allGroups.Any(group => group.Name == _group.Name)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Something went wrong!",
Text = "This group already exists!",
Icon = SweetAlertIcon.Error,
ShowConfirmButton = false,
Timer = 1500
});
return;
}
await Groups.CreatePermissionGroup(new PermissionGroup {
Description = _group.Description,
IsDefaultGroup = _group.IsDefaultGroup,
Permissions = _group.Permissions,
Name = "group." + _group.GroupName
});
if (ReloadPage is not null)
await ReloadPage.Invoke();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Group added!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
private async Task NoEditPermissions() {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to edit a group!",
Icon = SweetAlertIcon.Error
});
}
private async Task NoAddPermissions() {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to add a group!",
Icon = SweetAlertIcon.Error
});
}
}

View File

@@ -1,136 +0,0 @@
@rendermode InteractiveServer
@using BlazorStrap
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using BlazorStrap.Shared.Components.Modal
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Model
<BSModal DataId="add-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" OnShow="() => _user = new()" @ref="_modal">
<BSForm Model="_user" OnValidSubmit="AddUser">
<BSModalHeader>Add user</BSModalHeader>
<BSModalContent>
<div class="mb-3">
<BSLabel>E-Mail</BSLabel>
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
</div>
<div class="mb-3">
<BSLabel>Username</BSLabel>
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
</div>
<div class="mb-3">
<BSLabel>Password</BSLabel>
<BSInput InputType="InputType.Password" @bind-Value="_user.Password" required/>
</div>
<div class="mb-3">
<BSLabel>Primary group</BSLabel>
<BSInput InputType="InputType.Select" @bind-Value="_user.Group">
<option value="">Select group</option>
@foreach (var group in _allGroups) {
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
}
</BSInput>
</div>
</BSModalContent>
<BSModalFooter>
<BSButton Target="add-user-modal">Cancel</BSButton>
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
</BSModalFooter>
</BSForm>
</BSModal>
@inject IUserRepository Users
@inject IPermissionRepository Permissions
@inject IGroupRepository Groups
@inject SweetAlertService Alerts
@inject ITokenContext Auth
@code {
[Parameter] public Func<Task> ReloadPage { get; set; }
private IList<PermissionGroup> _allGroups = new List<PermissionGroup>();
private IList<User> _allUsers = new List<User>();
private UserAdd _user;
private BSModalBase _modal;
public async Task ShowAsync() {
_allGroups = await Groups.GetPermissionGroups();
_allUsers = await Users.GetUsers();
await _modal.ShowAsync();
}
private async Task AddUser() {
if (!(await Permissions.HasPermission(Auth.User, Security.AdminPermissions.AddUser))) {
await NoAddPermissions();
return;
}
string errorMessage = null;
if (_allUsers.Any(user => user.Username == _user.Username)) {
errorMessage = "Username is already taken!";
}
else if (_allUsers.Any(user => user.Email == _user.Email)) {
errorMessage = "E-Mail is already taken!";
}
else if (!_user.PasswordIsValid) {
errorMessage = "The password needs to be at least 8 characters long!";
}
else if (!_user.EmailIsValid) {
errorMessage = "Invalid E-Mail address!";
}
else if (string.IsNullOrWhiteSpace(_user.Username)) {
errorMessage = "You need to set a username!";
}
if (!string.IsNullOrWhiteSpace(errorMessage)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Something went wrong!",
Text = errorMessage,
Icon = SweetAlertIcon.Error,
ShowConfirmButton = false,
Timer = 1500
});
return;
}
var user = await Users.AddUser(new User {
Username = _user.Username,
Email = _user.Email,
Password = _user.Password
});
if (!string.IsNullOrWhiteSpace(_user.Group)) {
await Permissions.AddPermission(user, _user.Group);
}
await ReloadPage.Invoke();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "New user added!",
Icon = SweetAlertIcon.Success,
ShowConfirmButton = false,
Timer = 1500
});
}
private async Task NoAddPermissions() {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to add a user!",
Icon = SweetAlertIcon.Error
});
}
}

View File

@@ -1,306 +0,0 @@
@rendermode InteractiveServer
@using BlazorStrap
@using BlazorStrap.Shared.Components.Modal
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Model
<BSModal DataId="edit-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
<BSForm Model="_user" OnValidSubmit="EditUser">
<BSModalHeader>Edit @_user.Username</BSModalHeader>
<BSModalContent>
<div class="mb-3">
<BSLabel>User id</BSLabel>
<input type="text" class="form-control" disabled value="@_user.Id"/>
</div>
<div class="mb-3">
<BSLabel>Created at</BSLabel>
<input type="text" class="form-control" disabled value="@_user.CreatedAt"/>
</div>
<div class="mb-3">
<BSLabel>E-Mail</BSLabel>
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
</div>
<div class="mb-3">
<BSLabel>Username</BSLabel>
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
</div>
<div class="mb-3">
<BSLabel>Password</BSLabel>
<BSInput InputType="InputType.Password" @bind-Value="_newPassword"/>
</div>
<div class="mb-3">
<BSLabel>Groups</BSLabel>
<BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var group in _userGroups) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemoveGroup(group)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@group.Name.Replace("group.", "")</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div style="display: flex; gap: 20px">
<BSInput InputType="InputType.Select" @bind-Value="_selectedGroup">
<option selected>Select group</option>
@foreach (var group in _allGroups) {
@if (_userGroups?.All(g => g.Name != group.Name) == true) {
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
}
}
</BSInput>
<BSButton Color="BSColor.Secondary" OnClick="AddGroup">Add</BSButton>
</div>
</BSListGroupItem>
</BSListGroup>
</div>
<div class="mb-3">
<BSLabel>Permissions</BSLabel>
<BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var perm in _user.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@perm.PermissionName</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div style="display: flex; gap: 20px">
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
</div>
</BSListGroupItem>
</BSListGroup>
</div>
</BSModalContent>
<BSModalFooter>
<BSButton Target="edit-user-modal">Cancel</BSButton>
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
</BSModalFooter>
</BSForm>
</BSModal>
@inject IUserRepository Users
@inject IPermissionRepository Permissions
@inject IGroupRepository Groups
@inject SweetAlertService Alerts
@inject ITokenContext Auth
@code {
[Parameter] public Func<Task> ReloadPage { get; set; }
private BSModalBase _modal;
private User _user;
private string _newPassword;
private IList<PermissionGroup> _userGroups;
private IList<PermissionGroup> _allGroups;
private string _selectedGroup;
private string _permissionToAdd;
public async Task ShowAsync(User user) {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
_user = user;
_userGroups = await Groups.GetUserGroups(user);
_allGroups = await Groups.GetPermissionGroups();
await _modal.ShowAsync();
}
private async Task AddGroup() {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
if (string.IsNullOrWhiteSpace(_selectedGroup)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Select a group!",
Icon = SweetAlertIcon.Error,
ShowConfirmButton = true
});
return;
}
var group = _allGroups.SingleOrDefault(group => group.Name == _selectedGroup);
await Permissions.AddPermission(_user, group?.Name);
_userGroups.Add(group);
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Group added!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
private async Task RemoveGroup(PermissionGroup group) {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
var result = await Alerts.FireAsync(new SweetAlertOptions {
Title = "Are you sure?",
Icon = SweetAlertIcon.Warning,
ConfirmButtonText = "Yes",
ShowCancelButton = true,
ShowConfirmButton = true
});
if (result.IsConfirmed) {
await Permissions.RemovePermission(_user, group.Name);
_userGroups.Remove(group);
StateHasChanged();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Group removed!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
}
private async Task AddPermission() {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Enter a permission name!",
Icon = SweetAlertIcon.Error,
ShowConfirmButton = true
});
return;
}
_user.Permissions.Add(await Permissions.AddPermission(_user, _permissionToAdd));
_permissionToAdd = "";
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Permission added!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
private async Task RemovePermission(Permission perm) {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
var result = await Alerts.FireAsync(new SweetAlertOptions {
Title = "Are you sure?",
Icon = SweetAlertIcon.Warning,
ConfirmButtonText = "Yes",
ShowCancelButton = true,
ShowConfirmButton = true
});
if (result.IsConfirmed) {
await Permissions.RemovePermission(perm.User, perm.PermissionName);
_user.Permissions.Remove(perm);
StateHasChanged();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Permission removed!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
}
private async void EditUser() {
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
await NoEditPermissions();
return;
}
string errorMessage = null;
var validator = new RegisterData {
Password = _newPassword,
Email = _user.Email
};
var allUsers = await Users.GetUsers();
if (allUsers.Any(user => user.Username == _user.Username && user.Id != _user.Id)) {
errorMessage = "Username is already taken!";
}
else if (allUsers.Any(user => user.Email == _user.Email && user.Id != _user.Id)) {
errorMessage = "E-Mail is already taken!";
}
else if (!string.IsNullOrWhiteSpace(_newPassword) && !validator.PasswordIsValid) {
errorMessage = "The password needs to be at least 8 characters long!";
}
else if (!validator.EmailIsValid) {
errorMessage = "Invalid E-Mail address!";
}
if (!string.IsNullOrWhiteSpace(errorMessage)) {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Something went wrong!",
Text = errorMessage,
Icon = SweetAlertIcon.Error,
ShowConfirmButton = false,
Timer = 1500
});
return;
}
await Users.UpdateUser(_user);
if (!string.IsNullOrWhiteSpace(_newPassword)) {
await Users.ChangePassword(_user, _newPassword);
}
if (ReloadPage is not null)
await ReloadPage.Invoke();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "User edited!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
private async Task NoEditPermissions() {
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!",
Text = "You don't have the required permissions to edit a user!",
Icon = SweetAlertIcon.Error
});
}
}

View File

@@ -0,0 +1,88 @@
using System.Text.RegularExpressions;
using HopFrame.Database.Models;
using HopFrame.Security;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
using HopFrame.Web.Repositories;
namespace HopFrame.Web;
public class HopAdminContext : AdminPagesContext {
public AdminPage<User> Users { get; set; }
public AdminPage<PermissionGroup> Groups { get; set; }
public override void OnModelCreating(IAdminContextGenerator generator) {
generator.Page<User>()
.Description("On this page you can manage all user accounts.")
.ConfigureRepository<UserProvider>()
.ViewPermission(AdminPermissions.ViewUsers)
.CreatePermission(AdminPermissions.AddUser)
.UpdatePermission(AdminPermissions.EditUser)
.DeletePermission(AdminPermissions.DeleteUser);
generator.Page<User>().Property(u => u.Password)
.DisplayInListing(false)
.DisplayValueWhileEditing(false)
.Validator(passwd => passwd.Length >= 8 ? null : "The password needs to be at least 8 characters long!");
generator.Page<User>().Property(u => u.Email)
.Validator(email => Regex.Match(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").Success ? null : "Invalid E-Mail address!")
.Unique();
generator.Page<User>().Property(u => u.Username)
.Validator(uname => uname.Length >= 4 ? null : "The username needs to be at least 4 characters long!")
.Unique();
generator.Page<User>().Property(u => u.CreatedAt)
.Editable(false);
generator.Page<User>().Property(u => u.Permissions)
.DisplayInListing(false)
.DisplayProperty<Permission>(p => p.PermissionName)
.Parser<string, Permission>((user, perm) => new Permission {
GrantedAt = DateTime.Now,
PermissionName = perm,
User = user
});
generator.Page<User>().Property(u => u.CreatedAt)
.Generated();
generator.Page<User>().Property(u => u.Id)
.Generated();
generator.Page<User>().Property(u => u.Tokens)
.Ignore();
generator.Page<PermissionGroup>()
.Description("On this page you can view, create, edit and delete permission groups.")
.ConfigureRepository<GroupProvider>()
.ViewPermission(AdminPermissions.ViewGroups)
.CreatePermission(AdminPermissions.AddGroup)
.UpdatePermission(AdminPermissions.EditGroup)
.DeletePermission(AdminPermissions.DeleteGroup)
.ListingProperty(g => g.Name);
generator.Page<PermissionGroup>().Property(g => g.Name)
.Prefix("group.");
generator.Page<PermissionGroup>().Property(g => g.IsDefaultGroup)
.DisplayName("Default Group")
.Sortable(false);
generator.Page<PermissionGroup>().Property(g => g.CreatedAt)
.Generated();
generator.Page<PermissionGroup>().Property(g => g.Permissions)
.DisplayInListing(false)
.DisplayProperty<Permission>(p => p.PermissionName)
.Parser<string, Permission>((group, perm) => new Permission {
GrantedAt = DateTime.Now,
PermissionName = perm,
Group = group
});
}
}

View File

@@ -20,6 +20,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" /> <ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" /> <ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
<ProjectReference Include="..\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,8 +0,0 @@
namespace HopFrame.Web.Model;
public sealed class NavigationItem {
public string Name { get; set; }
public string Url { get; set; }
public string Permission { get; set; }
public string Description { get; set; }
}

View File

@@ -1,7 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Web.Model;
internal sealed class PermissionGroupAdd : PermissionGroup {
public string GroupName { get; set; }
}

View File

@@ -1,11 +0,0 @@
using HopFrame.Security.Models;
namespace HopFrame.Web.Model;
internal class RegisterData : UserRegister {
public string RepeatedPassword { get; set; }
public bool PasswordsMatch => Password == RepeatedPassword;
public bool PasswordIsValid => Password?.Length >= 8;
public bool EmailIsValid => Email?.Contains('@') == true && Email?.Contains('.') == true && Email?.EndsWith('.') == false;
}

View File

@@ -1,5 +0,0 @@
namespace HopFrame.Web.Model;
internal sealed class UserAdd : RegisterData {
public string Group { get; set; }
}

View File

@@ -5,6 +5,7 @@
@using BlazorStrap @using BlazorStrap
@using HopFrame.Web.Pages.Administration.Layout @using HopFrame.Web.Pages.Administration.Layout
@using BlazorStrap.V5 @using BlazorStrap.V5
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Components @using HopFrame.Web.Components
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@layout AdminLayout @layout AdminLayout
@@ -13,15 +14,15 @@
<BSContainer> <BSContainer>
<BSRow Justify="Justify.Center"> <BSRow Justify="Justify.Center">
@foreach (var view in AdminMenu.Subpages) { @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@view.Permission"> <AuthorizedView Permission="@adminPage.Permissions.View">
<BSCol Column="4" style="margin-bottom: 10px"> <BSCol Column="4" style="margin-bottom: 10px">
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px"> <BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px">
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column"> <BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
<BSCard CardType="CardType.Title">@view.Name</BSCard> <BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@view.Permission</span></BSCard> <BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
<BSCard CardType="CardType.Text">@view.Description</BSCard> <BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => Navigator.NavigateTo(view.Url, true)" Color="BSColor.Light">Open</BSButton> <BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
</BSCard> </BSCard>
</BSCard> </BSCard>
</BSCol> </BSCol>
@@ -31,3 +32,12 @@
</BSContainer> </BSContainer>
@inject NavigationManager Navigator @inject NavigationManager Navigator
@inject IAdminPagesProvider Pages
@code {
public void NavigateTo(string url) {
Navigator.NavigateTo("administration/" + url, true);
}
}

View File

@@ -0,0 +1,246 @@
@page "/administration/{url}"
@layout AdminLayout
@rendermode InteractiveServer
@using System.ComponentModel
@using BlazorStrap
@using Microsoft.AspNetCore.Components.Web
@using HopFrame.Web.Admin.Models
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Pages.Administration.Layout
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using HopFrame.Web.Components.Administration
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Admin
@using HopFrame.Web.Components
<PageTitle>@_pageData.Title</PageTitle>
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="administration/login" />
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>
<div class="title">
<h3>
@_pageData.Title administration
<span class="reload" @onclick="Reload">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
</span>
</h3>
<div class="d-flex" role="search" id="search">
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @oninput="TriggerSearch">
</div>
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
<BSButton IsSubmit="false" Color="BSColor.Success" @onclick="Create">Add Entry</BSButton>
</AuthorizedView>
</div>
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
<BSTHead>
<BSTR>
@foreach (var prop in GetListingProperties()) {
<BSTD>
@if (prop.Sortable) {
<span class="sorter" @onclick="() => OrderBy(prop.Name)">@prop.DisplayName</span>
@if (_currentSortProperty == prop.Name) {
<HopIconDisplay Type="_currentSortDirection == ListSortDirection.Descending ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
}
else {
@prop.DisplayName
}
</BSTD>
}
@if (_hasEditPermission || _hasDeletePermission) {
<BSTD>Actions</BSTD>
}
</BSTR>
</BSTHead>
<BSTBody>
@foreach (var entry in _displayedModels) {
<BSTR>
@foreach (var prop in GetListingProperties()) {
@if (prop.Bold) {
<BSTD Class="bold">
@GetPrintableValue(entry, prop)
</BSTD>
}
else {
<BSTD>
@GetPrintableValue(entry, prop)
</BSTD>
}
}
@if (_hasEditPermission || _hasDeletePermission) {
<BSTD>
<BSButtonGroup>
@if (_hasEditPermission) {
<BSButton Color="BSColor.Warning" OnClick="() => Edit(entry)">Edit</BSButton>
}
@if (_hasDeletePermission) {
<BSButton Color="BSColor.Danger" OnClick="() => Delete(entry)">Delete</BSButton>
}
</BSButtonGroup>
</BSTD>
}
</BSTR>
}
</BSTBody>
</BSTable>
<style>
.bold {
font-weight: bold;
}
</style>
@inject IAdminPagesProvider Pages
@inject IServiceProvider Provider
@inject ITokenContext Auth
@inject IPermissionRepository Permissions
@inject SweetAlertService Alerts
@code {
[Parameter]
public string Url { get; set; }
private AdminPage _pageData;
private IModelRepository _modelRepository;
private IEnumerable<object> _modelBuffer;
private AdminPageModal _modal;
private bool _hasEditPermission;
private bool _hasDeletePermission;
private string _currentSortProperty;
private ListSortDirection _currentSortDirection;
private DateTime _lastSearch;
private IList<object> _displayedModels;
protected override async Task OnInitializedAsync() {
_pageData = Pages.LoadAdminPage(Url);
_currentSortProperty = _pageData.DefaultSortPropertyName;
_currentSortDirection = _pageData.DefaultSortDirection;
if (_pageData.RepositoryProvider is null)
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelRepository = Provider.GetService(_pageData.RepositoryProvider) as IModelRepository;
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
await Reload();
}
private IList<AdminPageProperty> GetListingProperties() {
return _pageData.Properties
.Where(p => p.Ignore == false)
.Where(p => p.DisplayInListing)
.ToList();
}
private async Task Reload() {
_modelBuffer = await _modelRepository.ReadAllO();
_displayedModels = _modelBuffer.ToList();
_currentSortDirection = _pageData.DefaultSortDirection;
OrderBy(_pageData.DefaultSortPropertyName, false);
StateHasChanged();
}
private void OrderBy(string property, bool changeDir = true) {
if (_currentSortProperty == property && changeDir)
_currentSortDirection = (ListSortDirection)(((int)_currentSortDirection + 1) % 2);
if (_currentSortProperty != property)
_currentSortDirection = ListSortDirection.Ascending;
var prop = GetListingProperties()
.SingleOrDefault(p => p.Name == property);
var comparer = Comparer<object>.Create((x, y) => {
if (prop?.Type == typeof(DateTime)) {
DateTime dateX = (DateTime) x.GetType().GetProperty(prop.Name)?.GetValue(x)!;
DateTime dateY = (DateTime) y.GetType().GetProperty(prop.Name)?.GetValue(y)!;
return DateTime.Compare(dateX, dateY);
}
var propX = GetPrintableValue(x, prop);
var propY = GetPrintableValue(y, prop);
return String.CompareOrdinal(propX, propY);
});
_displayedModels = _currentSortDirection == ListSortDirection.Ascending ? _displayedModels.Order(comparer).ToList() : _displayedModels.OrderDescending(comparer).ToList();
_currentSortProperty = property;
}
private void TriggerSearch(ChangeEventArgs e) {
var search = ((string)e.Value)?.Trim();
Search(search);
}
private async void Search(string search) {
_lastSearch = DateTime.Now;
await Task.Delay(500);
var timeSinceLastKeyPress = DateTime.Now - _lastSearch;
if (timeSinceLastKeyPress < TimeSpan.FromMilliseconds(500)) return;
if (string.IsNullOrWhiteSpace(search)) {
_displayedModels = _modelBuffer.ToList();
}
else {
var props = GetListingProperties();
_displayedModels = _modelBuffer
.Where(model => props.Any(prop => GetPrintableValue(model, prop).Contains(search)))
.ToList();
}
OrderBy(_currentSortProperty, false);
StateHasChanged();
}
private string GetPrintableValue(object element, AdminPageProperty prop) {
return _modal?.MapPropertyValue(prop.GetValue(element), prop);
}
private async void Create() {
await _modal.Show(_pageData);
}
private async void Edit(object entry) {
await _modal.Show(_pageData, entry);
}
private async void Delete(object entry) {
var result = await Alerts.FireAsync(new SweetAlertOptions {
Title = "Are you sure?",
Text = "You won't be able to revert this!",
Icon = SweetAlertIcon.Warning,
ConfirmButtonText = "Yes",
ShowCancelButton = true,
ShowConfirmButton = true
});
if (result.IsConfirmed) {
await _modelRepository.DeleteO(entry);
await Reload();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Deleted!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
}
}

View File

@@ -9,7 +9,7 @@
margin-left: auto; margin-left: auto;
} }
th, h3 { th, h3, .sorter {
user-select: none; user-select: none;
} }
@@ -20,7 +20,3 @@ h3 {
.reload, .sorter { .reload, .sorter {
cursor: pointer; cursor: pointer;
} }
.bold {
font-weight: bold;
}

View File

@@ -1,192 +0,0 @@
@page "/administration/groups"
@rendermode InteractiveServer
@layout AdminLayout
@using System.Globalization
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using BlazorStrap
@using Microsoft.AspNetCore.Components.Web
@using HopFrame.Web.Components
@using HopFrame.Web.Components.Administration
@using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims
@using HopFrame.Web.Pages.Administration.Layout
<PageTitle>Groups</PageTitle>
<AuthorizedView Permission="@Security.AdminPermissions.ViewGroups" RedirectIfUnauthorized="administration/login?redirect=/administration/groups"/>
<GroupAddModal ReloadPage="Reload" @ref="_groupAddModal"/>
<div class="title">
<h3>
Groups administration
<span class="reload" @onclick="Reload">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
</span>
</h3>
<form class="d-flex" role="search" id="search" @onsubmit="Search">
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
</form>
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _groupAddModal.ShowAsync()">Add Group</BSButton>
</AuthorizedView>
</div>
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
<BSTHead>
<BSTR>
<BSTD>
<span class="sorter" @onclick="() => OrderBy(OrderType.Name)">Name</span>
@if (_currentOrder == OrderType.Name) {
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
</BSTD>
<BSTD>Description</BSTD>
<BSTD>Default</BSTD>
<BSTD>
<span class="sorter" @onclick="() => OrderBy(OrderType.Created)">Created</span>
@if (_currentOrder == OrderType.Created) {
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
</BSTD>
@if (_hasEditPrivileges || _hasDeletePrivileges) {
<BSTD>Actions</BSTD>
}
</BSTR>
</BSTHead>
<BSTBody>
@foreach (var group in _groups) {
<BSTR>
<BSTD Class="bold">@group.Name.Replace("group.", "")</BSTD>
<BSTD>@group.Description</BSTD>
<BSTD>
@if (group.IsDefaultGroup) {
<span>Yes</span>
}
else {
<span>No</span>
}
</BSTD>
<BSTD>@group.CreatedAt</BSTD>
@if (_hasEditPrivileges || _hasDeletePrivileges) {
<BSTD>
<BSButtonGroup>
@if (_hasEditPrivileges) {
<BSButton Color="BSColor.Warning" OnClick="() => _groupAddModal.ShowAsync(group)">Edit</BSButton>
}
@if (_hasDeletePrivileges) {
<BSButton Color="BSColor.Danger" OnClick="() => Delete(group)">Delete</BSButton>
}
</BSButtonGroup>
</BSTD>
}
</BSTR>
}
</BSTBody>
</BSTable>
@inject IGroupRepository Groups
@inject IPermissionRepository Permissions
@inject ITokenContext Auth
@inject SweetAlertService Alerts
@code {
private IList<PermissionGroup> _groups = new List<PermissionGroup>();
private bool _hasEditPrivileges = false;
private bool _hasDeletePrivileges = false;
private string _searchText;
private OrderType _currentOrder = OrderType.None;
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
private GroupAddModal _groupAddModal;
protected override async Task OnInitializedAsync() {
_groups = await Groups.GetPermissionGroups();
_hasEditPrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditGroup);
_hasDeletePrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.DeleteGroup);
}
private async Task Reload() {
_groups = new List<PermissionGroup>();
_groups = await Groups.GetPermissionGroups();
OrderBy(_currentOrder, false);
StateHasChanged();
}
private async Task Search() {
var groups = await Groups.GetPermissionGroups();
if (!string.IsNullOrWhiteSpace(_searchText)) {
groups = groups
.Where(group => group.Name.Contains(_searchText) ||
group.Description?.Contains(_searchText) == true ||
group.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
group.Permissions.Any(perm => perm.PermissionName.Contains(_searchText)))
.ToList();
}
_groups = groups;
OrderBy(_currentOrder, false);
}
private void OrderBy(OrderType type, bool changeDir = true) {
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
if (type == OrderType.Name) {
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.Name).ToList() : _groups.OrderByDescending(group => group.Name).ToList();
}
else if (type == OrderType.Created) {
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.CreatedAt).ToList() : _groups.OrderByDescending(group => group.CreatedAt).ToList();
}
_currentOrder = type;
}
private async Task Delete(PermissionGroup group) {
var result = await Alerts.FireAsync(new SweetAlertOptions {
Title = "Are you sure?",
Text = "You won't be able to revert this!",
Icon = SweetAlertIcon.Warning,
ConfirmButtonText = "Yes",
ShowCancelButton = true,
ShowConfirmButton = true
});
if (result.IsConfirmed) {
await Groups.DeletePermissionGroup(group);
await Reload();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Deleted!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
}
private enum OrderType {
None,
Name,
Created
}
private enum OrderDirection : byte {
Asc = 0,
Desc = 1
}
}

View File

@@ -3,10 +3,10 @@
@using BlazorStrap @using BlazorStrap
@using BlazorStrap.V5 @using BlazorStrap.V5
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Services @using HopFrame.Web.Services
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using HopFrame.Web.Components.Administration @using HopFrame.Web.Components.Administration
@using HopFrame.Web.Model
@using HopFrame.Web.Components @using HopFrame.Web.Components
@@ -23,9 +23,9 @@
<BSNav MarginEnd="Margins.Auto" Class="mb-lg-0"> <BSNav MarginEnd="Margins.Auto" Class="mb-lg-0">
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem> <BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
@foreach (var nav in Subpages) { @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@nav.Permission"> <AuthorizedView Permission="@adminPage.Permissions.View">
<BSNavItem IsActive="IsNavItemActive(nav.Url)" OnClick="() => Navigate(nav.Url)">@nav.Name</BSNavItem> <BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
</AuthorizedView> </AuthorizedView>
} }
</BSNav> </BSNav>
@@ -46,25 +46,11 @@
@inject NavigationManager Navigator @inject NavigationManager Navigator
@inject ITokenContext Context @inject ITokenContext Context
@inject IAuthService Auth @inject IAuthService Auth
@inject IAdminPagesProvider Pages
@code { @code {
public static IList<NavigationItem> Subpages = new List<NavigationItem> {
new () {
Name = "Users",
Url = "administration/users",
Description = "On this page you can manage all user accounts.",
Permission = Security.AdminPermissions.ViewUsers
},
new () {
Name = "Groups",
Url = "administration/groups",
Description = "On this page you can view, create, edit and delete permission groups.",
Permission = Security.AdminPermissions.ViewGroups
}
};
private bool IsNavItemActive(string element) { private bool IsNavItemActive(string element) {
return Navigator.Uri.Contains(element); return Navigator.Uri.TrimEnd('/').EndsWith(element);
} }
private bool IsDashboardActive() { private bool IsDashboardActive() {
@@ -72,11 +58,11 @@
} }
private void NavigateToDashboard() { private void NavigateToDashboard() {
Navigate("administration"); Navigator.NavigateTo("administration", true);
} }
private void Navigate(string url) { private void Navigate(string url) {
Navigator.NavigateTo(url, true); Navigator.NavigateTo("administration/" + url, true);
} }
private void Logout() { private void Logout() {

View File

@@ -1,222 +0,0 @@
@page "/administration/users"
@rendermode InteractiveServer
@layout AdminLayout
@using System.Globalization
@using BlazorStrap
@using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models
@using HopFrame.Security.Claims
@using HopFrame.Web.Pages.Administration.Layout
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web
@using HopFrame.Web.Components
@using BlazorStrap.V5
@using HopFrame.Database.Repositories
@using HopFrame.Web.Components.Administration
<PageTitle>Users</PageTitle>
<AuthorizedView Permission="@Security.AdminPermissions.ViewUsers" RedirectIfUnauthorized="administration/login?redirect=/administration/users"/>
<UserAddModal @ref="_userAddModal" ReloadPage="Reload"/>
<UserEditModal @ref="_userEditModal" ReloadPage="Reload"/>
<div class="title">
<h3>
Users administration
<span class="reload" @onclick="Reload">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
</span>
</h3>
<form class="d-flex" role="search" @onsubmit="Search" id="search">
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
</form>
<AuthorizedView Permission="@Security.AdminPermissions.AddUser">
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _userAddModal.ShowAsync()">Add User</BSButton>
</AuthorizedView>
</div>
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
<BSTHead>
<BSTR>
<BSTD>#</BSTD>
<BSTD>
<span class="sorter" @onclick="() => OrderBy(OrderType.Email)">E-Mail</span>
@if (_currentOrder == OrderType.Email) {
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
</BSTD>
<BSTD>
<span class="sorter" @onclick="() => OrderBy(OrderType.Username)">Username</span>
@if (_currentOrder == OrderType.Username) {
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
</BSTD>
<BSTD>
<span class="sorter" @onclick="() => OrderBy(OrderType.Registered)">Registered</span>
@if (_currentOrder == OrderType.Registered) {
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
}
</BSTD>
<BSTD>Primary Group</BSTD>
@if (_hasEditPrivileges || _hasDeletePrivileges) {
<BSTD>Actions</BSTD>
}
</BSTR>
</BSTHead>
<BSTBody>
@foreach (var user in _users) {
<BSTR>
<BSTD class="bold">@user.Id</BSTD>
<BSTD>@user.Email</BSTD>
<BSTD>@user.Username</BSTD>
<BSTD>@user.CreatedAt</BSTD>
<BSTD>@GetFriendlyGroupName(user)</BSTD>
@if (_hasEditPrivileges || _hasDeletePrivileges) {
<BSTD>
<BSButtonGroup>
@if (_hasEditPrivileges) {
<BSButton Color="BSColor.Warning" OnClick="() => _userEditModal.ShowAsync(user)">Edit</BSButton>
}
@if (_hasDeletePrivileges) {
<BSButton Color="BSColor.Danger" OnClick="() => Delete(user)">Delete</BSButton>
}
</BSButtonGroup>
</BSTD>
}
</BSTR>
}
</BSTBody>
</BSTable>
@inject IUserRepository UserService
@inject IPermissionRepository PermissionsService
@inject IGroupRepository Groups
@inject SweetAlertService Alerts
@inject ITokenContext Auth
@code {
private IList<User> _users = new List<User>();
private IDictionary<Guid, PermissionGroup> _userGroups = new Dictionary<Guid, PermissionGroup>();
private OrderType _currentOrder = OrderType.None;
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
private string _searchText;
private bool _hasEditPrivileges = false;
private bool _hasDeletePrivileges = false;
private UserAddModal _userAddModal;
private UserEditModal _userEditModal;
protected override async Task OnInitializedAsync() {
_users = await UserService.GetUsers();
foreach (var user in _users) {
var groups = await Groups.GetUserGroups(user);
_userGroups.Add(user.Id, groups.LastOrDefault());
}
_hasEditPrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.EditUser);
_hasDeletePrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.DeleteUser);
}
private async Task Reload() {
_users = new List<User>();
_userGroups = new Dictionary<Guid, PermissionGroup>();
_users = await UserService.GetUsers();
foreach (var user in _users) {
var groups = await Groups.GetUserGroups(user);
_userGroups.Add(user.Id, groups.LastOrDefault());
}
OrderBy(_currentOrder, false);
StateHasChanged();
}
private async Task Search() {
var users = await UserService.GetUsers();
if (!string.IsNullOrWhiteSpace(_searchText)) {
users = users
.Where(user =>
user.Email.Contains(_searchText) ||
user.Username.Contains(_searchText) ||
user.Id.ToString().Contains(_searchText) ||
user.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
_userGroups[user.Id]?.Name.Contains(_searchText) == true)
.ToList();
}
_users = users;
OrderBy(_currentOrder, false);
}
private string GetFriendlyGroupName(User user) {
var group = _userGroups[user.Id];
if (group is null) return null;
return group.Name.Replace("group.", "");
}
private void OrderBy(OrderType type, bool changeDir = true) {
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
if (type == OrderType.Email) {
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Email).ToList() : _users.OrderByDescending(user => user.Email).ToList();
}
else if (type == OrderType.Username) {
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Username).ToList() : _users.OrderByDescending(user => user.Username).ToList();
}
else if (type == OrderType.Registered) {
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.CreatedAt).ToList() : _users.OrderByDescending(user => user.CreatedAt).ToList();
}
_currentOrder = type;
}
private async Task Delete(User user) {
var result = await Alerts.FireAsync(new SweetAlertOptions {
Title = "Are you sure?",
Text = "You won't be able to revert this!",
Icon = SweetAlertIcon.Warning,
ConfirmButtonText = "Yes",
ShowCancelButton = true,
ShowConfirmButton = true
});
if (result.IsConfirmed) {
await UserService.DeleteUser(user);
await Reload();
await Alerts.FireAsync(new SweetAlertOptions {
Title = "Deleted!",
Icon = SweetAlertIcon.Success,
Timer = 1500,
ShowConfirmButton = false
});
}
}
private enum OrderType {
None,
Email,
Username,
Registered
}
private enum OrderDirection : byte {
Asc = 0,
Desc = 1
}
}

View File

@@ -1,26 +0,0 @@
.title {
display: flex;
flex-direction: row;
gap: 10px;
margin-bottom: 10px;
}
#search {
margin-left: auto;
}
th, h3 {
user-select: none;
}
h3 {
color: white;
}
.reload, .sorter {
cursor: pointer;
}
.bold {
font-weight: bold;
}

View File

@@ -0,0 +1,24 @@
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Web.Admin;
namespace HopFrame.Web.Repositories;
internal sealed class GroupProvider(IGroupRepository repo) : ModelRepository<PermissionGroup> {
public override async Task<IEnumerable<PermissionGroup>> ReadAll() {
return await repo.GetPermissionGroups();
}
public override async Task<PermissionGroup> Create(PermissionGroup model) {
return await repo.CreatePermissionGroup(model);
}
public override async Task<PermissionGroup> Update(PermissionGroup model) {
await repo.EditPermissionGroup(model);
return model;
}
public override Task Delete(PermissionGroup model) {
return repo.DeletePermissionGroup(model);
}
}

View File

@@ -0,0 +1,24 @@
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Web.Admin;
namespace HopFrame.Web.Repositories;
internal sealed class UserProvider(IUserRepository repo) : ModelRepository<User> {
public override async Task<IEnumerable<User>> ReadAll() {
return await repo.GetUsers();
}
public override Task<User> Create(User model) {
return repo.AddUser(model);
}
public override async Task<User> Update(User model) {
await repo.UpdateUser(model);
return model;
}
public override Task Delete(User model) {
return repo.DeleteUser(model);
}
}

View File

@@ -2,6 +2,7 @@ using BlazorStrap;
using CurrieTechnologies.Razor.SweetAlert2; using CurrieTechnologies.Razor.SweetAlert2;
using HopFrame.Database; using HopFrame.Database;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Web.Admin;
using HopFrame.Web.Services; using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation; using HopFrame.Web.Services.Implementation;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@@ -15,6 +16,7 @@ public static class ServiceCollectionExtensions {
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>(); services.AddScoped<IAuthService, AuthService>();
services.AddTransient<AuthMiddleware>(); services.AddTransient<AuthMiddleware>();
services.AddAdminContext<HopAdminContext>();
// Component library's // Component library's
services.AddSweetAlert2(); services.AddSweetAlert2();

View File

@@ -0,0 +1,38 @@
using FrontendTest.Providers;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
using RestApiTest.Models;
namespace FrontendTest;
public class AdminContext : AdminPagesContext {
public AdminPage<Address> Addresses { get; set; }
public AdminPage<Employee> Employees { get; set; }
public override void OnModelCreating(IAdminContextGenerator generator) {
base.OnModelCreating(generator);
generator.Page<Employee>()
.Property(e => e.Address)
.IsSelector();
generator.Page<Address>()
.Property(a => a.Employee)
.Ignore();
generator.Page<Address>()
.Property(a => a.AddressId)
.IsSelector<Employee>()
.Parser<Employee>((model, e) => model.AddressId = e.EmployeeId);
generator.Page<Employee>()
.ConfigureRepository<EmployeeProvider>()
.ListingProperty(e => e.Name);
generator.Page<Address>()
.ConfigureRepository<AddressProvider>()
.ListingProperty(a => a.City);
}
}

View File

@@ -1,4 +1,7 @@
@page "/counter" @page "/counter"
@using System.Text.Json
@using HopFrame.Web
@using HopFrame.Web.Admin.Providers
@rendermode InteractiveServer @rendermode InteractiveServer
<PageTitle>Counter</PageTitle> <PageTitle>Counter</PageTitle>
@@ -9,12 +12,20 @@
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@inject IAdminPagesProvider Provider
@code { @code {
private int currentCount = 0; private int currentCount = 0;
private string[] permissions = ["web.counter"]; private string[] permissions = ["web.counter"];
private void IncrementCount() { private void IncrementCount() {
currentCount++; currentCount++;
string json = JsonSerializer.Serialize(Provider.LoadRegisteredAdminPages(), new JsonSerializerOptions {
WriteIndented = true
});
Console.WriteLine(json);
} }
} }

View File

@@ -1,12 +1,24 @@
using HopFrame.Database; using HopFrame.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest; namespace FrontendTest;
public class DatabaseContext : HopDbContextBase { public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;"); optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Employee>()
.HasOne(e => e.Address)
.WithOne(a => a.Employee);
} }
} }

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace RestApiTest.Models;
public class Address {
[ForeignKey("Employee")]
public int AddressId { get; set; }
public string AddressDetails { get; set; }
public string City { get; set; }
public int ZipCode { get; set; }
public string State { get; set; }
public string Country { get; set; }
[JsonIgnore]
public virtual Employee Employee { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace RestApiTest.Models;
public class Employee {
public int EmployeeId { get; set; }
public string Name { get; set; }
public virtual Address Address { get; set; }
}

View File

@@ -1,11 +1,13 @@
using FrontendTest; using FrontendTest;
using FrontendTest.Components; using FrontendTest.Components;
using HopFrame.Web; using HopFrame.Web;
using HopFrame.Web.Admin;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DatabaseContext>(); builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>(); builder.Services.AddHopFrame<DatabaseContext>();
builder.Services.AddAdminContext<AdminContext>();
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()

View File

@@ -0,0 +1,29 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest.Providers;
public class AddressProvider(DatabaseContext context) : ModelRepository<Address> {
public override async Task<IEnumerable<Address>> ReadAll() {
return await context.Addresses.ToArrayAsync();
}
public override async Task<Address> Create(Address model) {
await context.Addresses.AddAsync(model);
await context.SaveChangesAsync();
return model;
}
public override async Task<Address> Update(Address model) {
context.Addresses.Update(model);
await context.SaveChangesAsync();
return model;
}
public override async Task Delete(Address model) {
context.Addresses.Remove(model);
await context.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,31 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest.Providers;
public class EmployeeProvider(DatabaseContext context) : ModelRepository<Employee> {
public override async Task<IEnumerable<Employee>> ReadAll() {
return await context.Employees
.Include(e => e.Address)
.ToArrayAsync();
}
public override async Task<Employee> Create(Employee model) {
await context.Employees.AddAsync(model);
await context.SaveChangesAsync();
return model;
}
public override async Task<Employee> Update(Employee model) {
context.Employees.Update(model);
await context.SaveChangesAsync();
return model;
}
public override async Task Delete(Employee model) {
context.Employees.Remove(model);
await context.SaveChangesAsync();
}
}

View File

@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;"); optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {