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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(NestedProjects) = preSolution
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">
<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;
&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;

View File

@@ -7,6 +7,14 @@ A simple backend management api for ASP.NET Core Web APIs
- [x] Permission management
- [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
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]
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)]
public Guid Id { get; init; }
[MaxLength(50)]
[Required, MaxLength(50)]
public string Username { get; set; }
[Required, MaxLength(50), EmailAddress]
@@ -20,9 +20,9 @@ public class User : IPermissionOwner {
[Required]
public DateTime CreatedAt { get; set; }
public virtual IList<Permission> Permissions { get; set; }
public virtual List<Permission> Permissions { get; set; }
[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>
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
<ProjectReference Include="..\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj" />
</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 HopFrame.Web.Pages.Administration.Layout
@using BlazorStrap.V5
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Components
@using Microsoft.AspNetCore.Components.Web
@layout AdminLayout
@@ -13,15 +14,15 @@
<BSContainer>
<BSRow Justify="Justify.Center">
@foreach (var view in AdminMenu.Subpages) {
<AuthorizedView Permission="@view.Permission">
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View">
<BSCol Column="4" style="margin-bottom: 10px">
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px">
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
<BSCard CardType="CardType.Title">@view.Name</BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@view.Permission</span></BSCard>
<BSCard CardType="CardType.Text">@view.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>
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
</BSCard>
</BSCard>
</BSCol>
@@ -31,3 +32,12 @@
</BSContainer>
@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;
}
th, h3 {
th, h3, .sorter {
user-select: none;
}
@@ -20,7 +20,3 @@ h3 {
.reload, .sorter {
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.V5
@using HopFrame.Security.Claims
@using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Services
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using HopFrame.Web.Components.Administration
@using HopFrame.Web.Model
@using HopFrame.Web.Components
@@ -23,9 +23,9 @@
<BSNav MarginEnd="Margins.Auto" Class="mb-lg-0">
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
@foreach (var nav in Subpages) {
<AuthorizedView Permission="@nav.Permission">
<BSNavItem IsActive="IsNavItemActive(nav.Url)" OnClick="() => Navigate(nav.Url)">@nav.Name</BSNavItem>
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View">
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
</AuthorizedView>
}
</BSNav>
@@ -46,25 +46,11 @@
@inject NavigationManager Navigator
@inject ITokenContext Context
@inject IAuthService Auth
@inject IAdminPagesProvider Pages
@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) {
return Navigator.Uri.Contains(element);
return Navigator.Uri.TrimEnd('/').EndsWith(element);
}
private bool IsDashboardActive() {
@@ -72,11 +58,11 @@
}
private void NavigateToDashboard() {
Navigate("administration");
Navigator.NavigateTo("administration", true);
}
private void Navigate(string url) {
Navigator.NavigateTo(url, true);
Navigator.NavigateTo("administration/" + url, true);
}
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 HopFrame.Database;
using HopFrame.Security.Authentication;
using HopFrame.Web.Admin;
using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation;
using Microsoft.AspNetCore.Builder;
@@ -15,6 +16,7 @@ public static class ServiceCollectionExtensions {
services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>();
services.AddTransient<AuthMiddleware>();
services.AddAdminContext<HopAdminContext>();
// Component library's
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"
@using System.Text.Json
@using HopFrame.Web
@using HopFrame.Web.Admin.Providers
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
@@ -9,12 +12,20 @@
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@inject IAdminPagesProvider Provider
@code {
private int currentCount = 0;
private string[] permissions = ["web.counter"];
private void IncrementCount() {
currentCount++;
string json = JsonSerializer.Serialize(Provider.LoadRegisteredAdminPages(), new JsonSerializerOptions {
WriteIndented = true
});
Console.WriteLine(json);
}
}

View File

@@ -1,12 +1,24 @@
using HopFrame.Database;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest;
public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder 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>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</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.Components;
using HopFrame.Web;
using HopFrame.Web.Admin;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
builder.Services.AddAdminContext<AdminContext>();
// Add services to the container.
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) {
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) {