Merge pull request #8 from leonhoppe/feature/generatedAdminPages
Feature/generated admin pages
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
||||
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
|
||||
namespace HopFrame.Web.Admin;
|
||||
|
||||
public abstract class AdminPagesContext {
|
||||
|
||||
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminHideValueAttribute : Attribute;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUneditableAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class AdminUniqueAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUnsortableAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ListingPropertyAttribute : Attribute;
|
||||
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal file
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal 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>();
|
||||
|
||||
}
|
||||
109
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal file
109
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal 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);
|
||||
|
||||
}
|
||||
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal file
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal 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);
|
||||
|
||||
}
|
||||
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal file
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal 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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal file
13
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal 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>
|
||||
33
src/HopFrame.Web.Admin/ModelRepository.cs
Normal file
33
src/HopFrame.Web.Admin/ModelRepository.cs
Normal 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);
|
||||
}
|
||||
26
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal file
26
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal 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;
|
||||
}
|
||||
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal file
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal 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; }
|
||||
}
|
||||
39
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal file
39
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal file
12
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
29
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal file
29
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
373
src/HopFrame.Web/Components/Administration/AdminPageModal.razor
Normal file
373
src/HopFrame.Web/Components/Administration/AdminPageModal.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
88
src/HopFrame.Web/HopAdminContext.cs
Normal file
88
src/HopFrame.Web/HopAdminContext.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
internal sealed class PermissionGroupAdd : PermissionGroup {
|
||||
public string GroupName { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
internal sealed class UserAdd : RegisterData {
|
||||
public string Group { get; set; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
246
src/HopFrame.Web/Pages/Administration/AdminPageList.razor
Normal file
246
src/HopFrame.Web/Pages/Administration/AdminPageList.razor
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
24
src/HopFrame.Web/Repositories/GroupProvider.cs
Normal file
24
src/HopFrame.Web/Repositories/GroupProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
24
src/HopFrame.Web/Repositories/UserProvider.cs
Normal file
24
src/HopFrame.Web/Repositories/UserProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
38
test/FrontendTest/AdminContext.cs
Normal file
38
test/FrontendTest/AdminContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
18
test/FrontendTest/Models/Address.cs
Normal file
18
test/FrontendTest/Models/Address.cs
Normal 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; }
|
||||
}
|
||||
8
test/FrontendTest/Models/Employee.cs
Normal file
8
test/FrontendTest/Models/Employee.cs
Normal 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; }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
29
test/FrontendTest/Providers/AddressProvider.cs
Normal file
29
test/FrontendTest/Providers/AddressProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal file
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user