diff --git a/HopFrame.sln b/HopFrame.sln index f9217e8..2e65007 100644 --- a/HopFrame.sln +++ b/HopFrame.sln @@ -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 diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index f66ed72..a38eed3 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -1,4 +1,7 @@  + ForceIncluded + ForceIncluded + ForceIncluded <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" /> diff --git a/README.md b/README.md index b7ed1b7..1366239 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/HopFrame.Database/Models/PermissionGroup.cs b/src/HopFrame.Database/Models/PermissionGroup.cs index 7a70ebd..aa0c92c 100644 --- a/src/HopFrame.Database/Models/PermissionGroup.cs +++ b/src/HopFrame.Database/Models/PermissionGroup.cs @@ -17,6 +17,6 @@ public class PermissionGroup : IPermissionOwner { [Required] public DateTime CreatedAt { get; set; } - public virtual IList Permissions { get; set; } + public virtual List Permissions { get; set; } } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/User.cs b/src/HopFrame.Database/Models/User.cs index 971d899..feec39c 100644 --- a/src/HopFrame.Database/Models/User.cs +++ b/src/HopFrame.Database/Models/User.cs @@ -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 Permissions { get; set; } + public virtual List Permissions { get; set; } [JsonIgnore] - public virtual IList Tokens { get; set; } + public virtual List Tokens { get; set; } } \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/AdminPagesContext.cs b/src/HopFrame.Web.Admin/AdminPagesContext.cs new file mode 100644 index 0000000..39b769c --- /dev/null +++ b/src/HopFrame.Web.Admin/AdminPagesContext.cs @@ -0,0 +1,9 @@ +using HopFrame.Web.Admin.Generators; + +namespace HopFrame.Web.Admin; + +public abstract class AdminPagesContext { + + public virtual void OnModelCreating(IAdminContextGenerator generator) {} + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs b/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs new file mode 100644 index 0000000..92e3fe3 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs b/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs new file mode 100644 index 0000000..90821c3 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs new file mode 100644 index 0000000..ccd3c8f --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs new file mode 100644 index 0000000..7d68eab --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs @@ -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 + }; +} diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs new file mode 100644 index 0000000..ab87a2e --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs new file mode 100644 index 0000000..ffc3798 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs new file mode 100644 index 0000000..deb8092 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Admin.Attributes.Members; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class AdminHideValueAttribute : Attribute; diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs new file mode 100644 index 0000000..7b33e04 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs @@ -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; +} diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminPrefixAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminPrefixAttribute.cs new file mode 100644 index 0000000..a5247db --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminPrefixAttribute.cs @@ -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; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs new file mode 100644 index 0000000..78eb491 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Admin.Attributes.Members; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class AdminUneditableAttribute : Attribute; diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminUniqueAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminUniqueAttribute.cs new file mode 100644 index 0000000..5247777 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminUniqueAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Admin.Attributes.Members; + +[AttributeUsage(AttributeTargets.Property)] +public class AdminUniqueAttribute : Attribute; \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs new file mode 100644 index 0000000..37935b2 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Admin.Attributes.Members; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class AdminUnsortableAttribute : Attribute; diff --git a/src/HopFrame.Web.Admin/Attributes/Members/ListingPropertyAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/ListingPropertyAttribute.cs new file mode 100644 index 0000000..eeb10f8 --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/Members/ListingPropertyAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Admin.Attributes.Members; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class ListingPropertyAttribute : Attribute; \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs new file mode 100644 index 0000000..970d82c --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs @@ -0,0 +1,12 @@ +namespace HopFrame.Web.Admin.Generators; + +public interface IAdminContextGenerator { + + /// + /// Returns the generator object for the specified Admin Page. This needs to be within the same Admin Context. + /// + /// The Model of the Admin Page + /// + IAdminPageGenerator Page(); + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs new file mode 100644 index 0000000..2a71a75 --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs @@ -0,0 +1,109 @@ +using System.ComponentModel; +using System.Linq.Expressions; + +namespace HopFrame.Web.Admin.Generators; + +public interface IAdminPageGenerator { + + /// + /// Sets the title of the Admin Page + /// + /// the specified title + /// + IAdminPageGenerator Title(string title); + + /// + /// Sets the description of the Admin Page + /// + /// the specified description + /// + IAdminPageGenerator Description(string description); + + /// + /// Sets the url for the Admin Page + /// + /// the specified url (administration/{url}) + /// + IAdminPageGenerator Url(string url); + + /// + /// Sets the permission needed to view the Admin Page + /// + /// the specified permission + /// + IAdminPageGenerator ViewPermission(string permission); + + /// + /// Sets the permission needed to create a new Entry + /// + /// the specified permission + /// + IAdminPageGenerator CreatePermission(string permission); + + /// + /// Sets the permission needed to update an Entry + /// + /// the specified permission + /// + IAdminPageGenerator UpdatePermission(string permission); + + /// + /// Sets the permission needed to delete an Entry + /// + /// the specified permission + /// + IAdminPageGenerator DeletePermission(string permission); + + + /// + /// Enables or disables the create button + /// + /// the specified state + /// + IAdminPageGenerator ShowCreateButton(bool show); + + /// + /// Enables or disables the delete button + /// + /// the specified state + /// + IAdminPageGenerator ShowDeleteButton(bool show); + + /// + /// Enables or disables the update button + /// + /// the specified state + /// + IAdminPageGenerator ShowUpdateButton(bool show); + + /// + /// Specifies the default sort property and direction + /// + /// Which property should be sorted + /// In which direction should be sorted + /// + IAdminPageGenerator DefaultSort(Expression> propertyExpression, ListSortDirection direction); + + /// + /// Specifies the repository for the page + /// + /// The specified repository + /// + IAdminPageGenerator ConfigureRepository() where TRepository : ModelRepository; + + + /// + /// Returns the generator of the specified property + /// + /// The property + /// + IAdminPropertyGenerator Property(Expression> propertyExpression); + + /// + /// Specifies the default property that should be displayed as a property in other listings + /// + /// The property + /// + IAdminPageGenerator ListingProperty(Expression> propertyExpression); + +} diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs new file mode 100644 index 0000000..4fbab5b --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs @@ -0,0 +1,123 @@ +using System.Linq.Expressions; + +namespace HopFrame.Web.Admin.Generators; + +public interface IAdminPropertyGenerator { + + /// + /// Should the property be sortable or not + /// + /// + IAdminPropertyGenerator Sortable(bool sortable); + + /// + /// Should the admin be able to edit the property after creation or not + /// + /// + IAdminPropertyGenerator Editable(bool editable); + + /// + /// Should the value of the property be displayed while editing or not (useful for passwords and tokens) + /// + /// + IAdminPropertyGenerator DisplayValueWhileEditing(bool display); + + /// + /// Should the property be a column on the page list or not + /// + /// + IAdminPropertyGenerator DisplayInListing(bool display = true); + + /// + /// Should the property be ignored completely + /// + /// + IAdminPropertyGenerator Ignore(bool ignore = true); + + /// + /// Is the value of the property database generated and is not meant to be changed + /// + /// + IAdminPropertyGenerator Generated(bool generated = true); + + /// + /// Should the property value be bold in the listing or not + /// + /// + IAdminPropertyGenerator Bold(bool bold = true); + + /// + /// Is the value of the property unique under all other entries in the dataset + /// + /// + /// + IAdminPropertyGenerator Unique(bool unique = true); + + /// + /// Specifies the display name in the listing and editing/creation + /// + /// + IAdminPropertyGenerator DisplayName(string displayName); + + /// + /// Has the value of the property a never changing prefix that doesn't need to be specified or displayed + /// + /// + IAdminPropertyGenerator Prefix(string prefix); + + /// + /// The specified function gets called before creation/edit to verify that the entered value matches the property requirements + /// + /// + IAdminPropertyGenerator Validator(Func validator); + + /// + /// 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! + /// + /// + IAdminPropertyGenerator IsSelector(bool selector = true); + + /// + /// 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! + /// + /// + /// + /// + IAdminPropertyGenerator IsSelector(bool selector = true); + + /// + /// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type + /// + /// + IAdminPropertyGenerator Parser(Func parser); + + /// + /// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type + /// + /// Needs to be specified if the field is not a plain string field (like a selector with a different type) + /// + IAdminPropertyGenerator Parser(Func parser); + + /// + /// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type + /// + /// Needs to be specified if the field is not a plain string field (like a selector with a different type) + /// Needs to be specified if the property type is a List + /// + IAdminPropertyGenerator Parser(Func parser); + + /// + /// Specifies the default property that should be displayed as a value + /// + /// + /// + IAdminPropertyGenerator DisplayProperty(Expression> propertyExpression); + + /// + /// Specifies the default property that should be displayed as a value + /// + /// Needs to be specified if the property type is a List + /// + IAdminPropertyGenerator DisplayProperty(Expression> propertyExpression); + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/IGenerator.cs b/src/HopFrame.Web.Admin/Generators/IGenerator.cs new file mode 100644 index 0000000..68f5013 --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/IGenerator.cs @@ -0,0 +1,11 @@ +namespace HopFrame.Web.Admin.Generators; + +public interface IGenerator { + + /// + /// Compiles the generator with all specified options + /// + /// The compiled data structure + TGeneratedType Compile(); + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs new file mode 100644 index 0000000..ae62af6 --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs @@ -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 _adminPages = new Dictionary(); + + public IAdminPageGenerator Page() { + if (_adminPages.TryGetValue(typeof(TModel), out var pageGenerator)) + return pageGenerator as IAdminPageGenerator; + + var generator = Activator.CreateInstance(typeof(IAdminPageGenerator)) as AdminPageGenerator; + generator?.ApplyConfigurationFromAttributes(typeof(TModel).GetCustomAttributes(false)); + + _adminPages.Add(typeof(TModel), generator); + + return generator; + } + + public AdminPage CompilePage() { + var generator = _adminPages[typeof(TModel)]; + if (generator is null) return null; + + return (generator as AdminPageGenerator)?.Compile(); + } + + public TContext CompileContext() where TContext : AdminPagesContext { + var type = typeof(TContext); + var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage)); + + var properties = type.GetProperties(); + + var context = Activator.CreateInstance(); + + 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.Title)); + titleMethod?.Invoke(generatorInstance, [property.Name]); + + var populateMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator.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); + } + } + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs new file mode 100644 index 0000000..2718e15 --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs @@ -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 : IAdminPageGenerator, IGenerator> { + + public readonly AdminPage Page; + private readonly Dictionary _propertyGenerators; + + public AdminPageGenerator() { + Page = new AdminPage { + Permissions = new AdminPagePermissions(), + ModelType = typeof(TModel) + }; + _propertyGenerators = new Dictionary(); + + 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.ApplyConfigurationFromAttributes))? + .MakeGenericMethod(type); + method?.Invoke(generator, [this, attributes, property]); + + _propertyGenerators.Add(property.Name, generator); + } + } + + public IAdminPageGenerator Title(string title) { + Page.Title = title; + Page.Url ??= title.ToLower(); + return this; + } + + public IAdminPageGenerator Description(string description) { + Page.Description = description; + return this; + } + + public IAdminPageGenerator Url(string url) { + Page.Url = url; + return this; + } + + public IAdminPageGenerator ViewPermission(string permission) { + Page.Permissions.View = permission; + return this; + } + + public IAdminPageGenerator CreatePermission(string permission) { + Page.Permissions.Create = permission; + return this; + } + + public IAdminPageGenerator UpdatePermission(string permission) { + Page.Permissions.Update = permission; + return this; + } + + public IAdminPageGenerator DeletePermission(string permission) { + Page.Permissions.Delete = permission; + return this; + } + + public IAdminPageGenerator ShowCreateButton(bool show) { + Page.ShowCreateButton = show; + return this; + } + + public IAdminPageGenerator ShowDeleteButton(bool show) { + Page.ShowDeleteButton = show; + return this; + } + + public IAdminPageGenerator ShowUpdateButton(bool show) { + Page.ShowUpdateButton = show; + return this; + } + + public IAdminPageGenerator DefaultSort(Expression> propertyExpression, ListSortDirection direction) { + var property = GetPropertyInfo(propertyExpression); + + Page.DefaultSortPropertyName = property.Name; + Page.DefaultSortDirection = direction; + return this; + } + + public IAdminPageGenerator ConfigureRepository() where TRepository : ModelRepository { + Page.RepositoryProvider = typeof(TRepository); + return this; + } + + public IAdminPropertyGenerator Property(Expression> propertyExpression) { + var property = GetPropertyInfo(propertyExpression); + + if (_propertyGenerators.TryGetValue(property.Name, out var propertyGenerator)) + return propertyGenerator as AdminPropertyGenerator; + + var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator), new { property.Name, property.PropertyType }) as AdminPropertyGenerator; + generator?.ApplyConfigurationFromAttributes(this, property.GetCustomAttributes(false), property); + _propertyGenerators.Add(property.Name, generator); + + return generator; + } + + public IAdminPageGenerator ListingProperty(Expression> propertyExpression) { + var property = GetPropertyInfo(propertyExpression); + Page.ListingProperty = property.Name; + return this; + } + + public AdminPage Compile() { + var properties = new List(); + + foreach (var generator in _propertyGenerators.Values) { + var method = generator.GetType().GetMethod(nameof(AdminPropertyGenerator.Compile)); + var prop = method?.Invoke(generator, []) as AdminPageProperty; + properties.Add(prop); + } + + Page.Properties = properties; + + return Page; + } + + public static PropertyInfo GetPropertyInfo(Expression> 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); + } + } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs new file mode 100644 index 0000000..d6a5792 --- /dev/null +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs @@ -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(string name, Type type) : IAdminPropertyGenerator, IGenerator { + + private readonly AdminPageProperty _property = new() { + Name = name, + Type = type + }; + + public IAdminPropertyGenerator Sortable(bool sortable) { + _property.Sortable = sortable; + return this; + } + + public IAdminPropertyGenerator Editable(bool editable) { + _property.Editable = editable; + return this; + } + + public IAdminPropertyGenerator DisplayValueWhileEditing(bool display) { + _property.EditDisplayValue = display; + return this; + } + + public IAdminPropertyGenerator DisplayInListing(bool display = true) { + _property.DisplayInListing = display; + _property.Sortable = false; + return this; + } + + public IAdminPropertyGenerator Ignore(bool ignore = false) { + _property.Ignore = ignore; + return this; + } + + public IAdminPropertyGenerator Generated(bool generated = true) { + _property.Generated = generated; + return this; + } + + public IAdminPropertyGenerator Bold(bool bold = true) { + _property.Bold = bold; + return this; + } + + public IAdminPropertyGenerator Unique(bool unique = true) { + _property.Unique = unique; + return this; + } + + public IAdminPropertyGenerator DisplayName(string displayName) { + _property.DisplayName = displayName; + return this; + } + + public IAdminPropertyGenerator Prefix(string prefix) { + _property.Prefix = prefix; + return this; + } + + public IAdminPropertyGenerator Validator(Func validator) { + _property.Validator = o => validator.Invoke((TProperty)o); + return this; + } + + public IAdminPropertyGenerator IsSelector(bool selector = true) { + _property.Selector = selector; + return this; + } + + public IAdminPropertyGenerator IsSelector(bool selector = true) { + _property.Selector = true; + _property.SelectorType = typeof(TSelectorType); + return this; + } + + public IAdminPropertyGenerator Parser(Func parser) { + _property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString()); + return this; + } + + public IAdminPropertyGenerator Parser(Func parser) { + _property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s); + return this; + } + + public IAdminPropertyGenerator Parser(Func parser) { + _property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s); + return this; + } + + public IAdminPropertyGenerator DisplayProperty(Expression> propertyExpression) { + var property = AdminPageGenerator.GetPropertyInfo(propertyExpression); + _property.DisplayPropertyName = property.Name; + return this; + } + + public IAdminPropertyGenerator DisplayProperty(Expression> propertyExpression) { + var property = AdminPageGenerator.GetPropertyInfo(propertyExpression); + _property.DisplayPropertyName = property.Name; + return this; + } + + public AdminPageProperty Compile() { + _property.DisplayName ??= _property.Name; + return _property; + } + + public void ApplyConfigurationFromAttributes(AdminPageGenerator 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; + } + } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj b/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj new file mode 100644 index 0000000..096a35f --- /dev/null +++ b/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + disable + + + + + + + diff --git a/src/HopFrame.Web.Admin/ModelRepository.cs b/src/HopFrame.Web.Admin/ModelRepository.cs new file mode 100644 index 0000000..45de247 --- /dev/null +++ b/src/HopFrame.Web.Admin/ModelRepository.cs @@ -0,0 +1,33 @@ +namespace HopFrame.Web.Admin; + +public abstract class ModelRepository : IModelRepository { + public abstract Task> ReadAll(); + public abstract Task Create(TModel model); + public abstract Task Update(TModel model); + public abstract Task Delete(TModel model); + + + public async Task> ReadAllO() { + var models = await ReadAll(); + return models.Select(m => (object)m); + } + + public async Task CreateO(object model) { + return await Create((TModel)model); + } + + public async Task UpdateO(object model) { + return await Update((TModel)model); + } + + public Task DeleteO(object model) { + return Delete((TModel)model); + } +} + +public interface IModelRepository { + Task> ReadAllO(); + Task CreateO(object model); + Task UpdateO(object model); + Task DeleteO(object model); +} diff --git a/src/HopFrame.Web.Admin/Models/AdminPage.cs b/src/HopFrame.Web.Admin/Models/AdminPage.cs new file mode 100644 index 0000000..748eb00 --- /dev/null +++ b/src/HopFrame.Web.Admin/Models/AdminPage.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace HopFrame.Web.Admin.Models; + +public sealed class AdminPage : 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 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; +} diff --git a/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs new file mode 100644 index 0000000..e9629a6 --- /dev/null +++ b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs @@ -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; } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs new file mode 100644 index 0000000..8347e36 --- /dev/null +++ b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs @@ -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 Validator { get; set; } + public Func Parser { get; set; } + + public object GetValue(object entry) { + return entry.GetType().GetProperty(Name)?.GetValue(entry); + } + + public T GetValue(object entry) { + return (T)entry.GetType().GetProperty(Name)?.GetValue(entry); + } + + public void SetValue(object entry, object value) { + entry.GetType().GetProperty(Name)?.SetValue(entry, value); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs b/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs new file mode 100644 index 0000000..564f52a --- /dev/null +++ b/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs @@ -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 LoadRegisteredAdminPages(); + AdminPage HasPageFor(Type type); + +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs b/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs new file mode 100644 index 0000000..b2e38b4 --- /dev/null +++ b/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs @@ -0,0 +1,26 @@ +using HopFrame.Web.Admin.Models; + +namespace HopFrame.Web.Admin.Providers.Implementation; + +public class AdminPagesProvider : IAdminPagesProvider { + private readonly IDictionary _pages = new Dictionary(); + + 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 LoadRegisteredAdminPages() { + return _pages.Values.ToList(); + } + + public AdminPage HasPageFor(Type type) { + return _pages + .Where(p => p.Value.ModelType == type) + .Select(p => p.Value) + .SingleOrDefault(); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f3e8370 --- /dev/null +++ b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs @@ -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(this IServiceCollection services) where TContext : AdminPagesContext { + var provider = GetProvider(); + services.TryAddSingleton(provider); + + var generator = new AdminContextGenerator(); + var context = generator.CompileContext(); + AdminContextGenerator.RegisterPages(context, provider, services); + services.AddSingleton(context); + + return services; + } + + private static IAdminPagesProvider GetProvider() { + return _provider ??= new AdminPagesProvider(); + } + +} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor new file mode 100644 index 0000000..f410739 --- /dev/null +++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor @@ -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 + + + + @if (!_isEdit) { + Create entry + } + else { + Edit entry + } + + + @foreach (var prop in GetEditableProperties()) { + @if (!_isEdit && prop.Generated) continue; + +
+ @if (IsListType(prop)) { + @prop.DisplayName + + + + @foreach (var element in GetListPropertyValues(prop).Select((e, i) => new { e, i })) { + + + + + + @element.e + + } + + + +
+ @if (!prop.Selector) { +
+ + Add +
+ } + else { +
+ + Add +
+ } +
+
+
+ } + else if (IsSwitch(prop)) { +
+ @prop.DisplayName + +
+ } + else if (prop.Prefix is not null && !_isEdit) { + + @prop.Prefix + + + } + else if (prop.Selector) { + @prop.DisplayName + + } + else { + @prop.DisplayName + + @if (_validation[_validationIdentifiers[prop]].Any()) { +
+ @_validation[_validationIdentifiers[prop]].First() +
+ } + } +
+ } +
+ + + Cancel + Save + +
+
+ +@inject IServiceProvider Provider +@inject IAdminPagesProvider PageProvider +@inject SweetAlertService Alerts +@inject IPermissionRepository Permissions +@inject ITokenContext Auth + +@code { + #pragma warning disable CS4014 + + [Parameter] + public Func ReloadDelegate { get; set; } + + private BSModalBase _modal; + private EditContext _context; + private ValidationMessageStore _validation; + private Dictionary _validationIdentifiers; + private IDictionary _values; + private Dictionary _selectorValues; + private IModelRepository _repository; + + private AdminPage _currentPage; + private object _entry; + private bool _isEdit; + private IDictionary _inputValues; + + public async Task Show(AdminPage page, object entryToEdit = null) { + _entry = null; + _inputValues = new Dictionary(); + _selectorValues = new Dictionary(); + + _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(); + _context.OnValidationRequested += Validate; + + _values = new Dictionary(); + 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 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 GetListPropertyValues(AdminPageProperty prop) { + if (!IsListType(prop)) return new List(); + var list = new List(); + + 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(_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(_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(); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Administration/GroupAddModal.razor b/src/HopFrame.Web/Components/Administration/GroupAddModal.razor deleted file mode 100644 index 8f432e7..0000000 --- a/src/HopFrame.Web/Components/Administration/GroupAddModal.razor +++ /dev/null @@ -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 - - - - @if (_isEdit) { - Edit group - } - else { - Add group - } - -
- Name - @if (!_isEdit) { - - group. - - - } - else { - - } -
- - @if (_isEdit) { -
- Created at - -
- } - -
- Description - -
- -
- - Default group - -
- -
- Inherits from - - - - @foreach (var group in _group.Permissions.Where(g => g.PermissionName.StartsWith("group."))) { - - - - - - @group.PermissionName.Replace("group.", "") - - } - - - -
- - - - @foreach (var group in _allGroups) { - @if (_group.Permissions.All(g => g.PermissionName != group.Name) && group.Name != _group.Name) { - - } - } - - Add -
-
-
-
- -
- Permissions - - - - @foreach (var perm in _group.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) { - - - - - - @perm.PermissionName - - } - - - -
- - Add -
-
-
-
-
- - Cancel - Save - -
-
- -@inject IGroupRepository Groups -@inject IPermissionRepository Permissions -@inject SweetAlertService Alerts -@inject ITokenContext Context - -@code { - [Parameter] public Func ReloadPage { get; set; } - - private PermissionGroupAdd _group; - - private BSModalBase _modal; - private string _permissionToAdd; - private string _groupToAdd; - - private IList _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(), - 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 - }); - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Administration/UserAddModal.razor b/src/HopFrame.Web/Components/Administration/UserAddModal.razor deleted file mode 100644 index 44b47b3..0000000 --- a/src/HopFrame.Web/Components/Administration/UserAddModal.razor +++ /dev/null @@ -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 - - - - Add user - -
- E-Mail - -
- -
- Username - -
- -
- Password - -
- -
- Primary group - - - - @foreach (var group in _allGroups) { - - } - -
-
- - Cancel - Save - -
-
- -@inject IUserRepository Users -@inject IPermissionRepository Permissions -@inject IGroupRepository Groups -@inject SweetAlertService Alerts -@inject ITokenContext Auth - -@code { - [Parameter] public Func ReloadPage { get; set; } - - private IList _allGroups = new List(); - private IList _allUsers = new List(); - 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 - }); - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Administration/UserEditModal.razor b/src/HopFrame.Web/Components/Administration/UserEditModal.razor deleted file mode 100644 index f4bbe36..0000000 --- a/src/HopFrame.Web/Components/Administration/UserEditModal.razor +++ /dev/null @@ -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 - - - - Edit @_user.Username - -
- User id - -
-
- Created at - -
-
- E-Mail - -
-
- Username - -
-
- Password - -
- -
- Groups - - - - @foreach (var group in _userGroups) { - - - - - - @group.Name.Replace("group.", "") - - } - - - -
- - - - @foreach (var group in _allGroups) { - @if (_userGroups?.All(g => g.Name != group.Name) == true) { - - } - } - - Add -
-
-
-
- -
- Permissions - - - - @foreach (var perm in _user.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) { - - - - - - @perm.PermissionName - - } - - - -
- - Add -
-
-
-
-
- - Cancel - Save - -
-
- -@inject IUserRepository Users -@inject IPermissionRepository Permissions -@inject IGroupRepository Groups -@inject SweetAlertService Alerts -@inject ITokenContext Auth - -@code { - [Parameter] public Func ReloadPage { get; set; } - - private BSModalBase _modal; - private User _user; - private string _newPassword; - - private IList _userGroups; - private IList _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 - }); - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/HopAdminContext.cs b/src/HopFrame.Web/HopAdminContext.cs new file mode 100644 index 0000000..e0ab493 --- /dev/null +++ b/src/HopFrame.Web/HopAdminContext.cs @@ -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 Users { get; set; } + public AdminPage Groups { get; set; } + + public override void OnModelCreating(IAdminContextGenerator generator) { + generator.Page() + .Description("On this page you can manage all user accounts.") + .ConfigureRepository() + .ViewPermission(AdminPermissions.ViewUsers) + .CreatePermission(AdminPermissions.AddUser) + .UpdatePermission(AdminPermissions.EditUser) + .DeletePermission(AdminPermissions.DeleteUser); + + generator.Page().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().Property(u => u.Email) + .Validator(email => Regex.Match(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").Success ? null : "Invalid E-Mail address!") + .Unique(); + + generator.Page().Property(u => u.Username) + .Validator(uname => uname.Length >= 4 ? null : "The username needs to be at least 4 characters long!") + .Unique(); + + generator.Page().Property(u => u.CreatedAt) + .Editable(false); + + generator.Page().Property(u => u.Permissions) + .DisplayInListing(false) + .DisplayProperty(p => p.PermissionName) + .Parser((user, perm) => new Permission { + GrantedAt = DateTime.Now, + PermissionName = perm, + User = user + }); + + generator.Page().Property(u => u.CreatedAt) + .Generated(); + + generator.Page().Property(u => u.Id) + .Generated(); + + generator.Page().Property(u => u.Tokens) + .Ignore(); + + + generator.Page() + .Description("On this page you can view, create, edit and delete permission groups.") + .ConfigureRepository() + .ViewPermission(AdminPermissions.ViewGroups) + .CreatePermission(AdminPermissions.AddGroup) + .UpdatePermission(AdminPermissions.EditGroup) + .DeletePermission(AdminPermissions.DeleteGroup) + .ListingProperty(g => g.Name); + + generator.Page().Property(g => g.Name) + .Prefix("group."); + + generator.Page().Property(g => g.IsDefaultGroup) + .DisplayName("Default Group") + .Sortable(false); + + generator.Page().Property(g => g.CreatedAt) + .Generated(); + + generator.Page().Property(g => g.Permissions) + .DisplayInListing(false) + .DisplayProperty(p => p.PermissionName) + .Parser((group, perm) => new Permission { + GrantedAt = DateTime.Now, + PermissionName = perm, + Group = group + }); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/HopFrame.Web.csproj b/src/HopFrame.Web/HopFrame.Web.csproj index ab3c31a..bec6f04 100644 --- a/src/HopFrame.Web/HopFrame.Web.csproj +++ b/src/HopFrame.Web/HopFrame.Web.csproj @@ -20,6 +20,7 @@ + diff --git a/src/HopFrame.Web/Model/NavigationItem.cs b/src/HopFrame.Web/Model/NavigationItem.cs deleted file mode 100644 index 6e255a0..0000000 --- a/src/HopFrame.Web/Model/NavigationItem.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Model/PermissionGroupAdd.cs b/src/HopFrame.Web/Model/PermissionGroupAdd.cs deleted file mode 100644 index 0cdc9d2..0000000 --- a/src/HopFrame.Web/Model/PermissionGroupAdd.cs +++ /dev/null @@ -1,7 +0,0 @@ -using HopFrame.Database.Models; - -namespace HopFrame.Web.Model; - -internal sealed class PermissionGroupAdd : PermissionGroup { - public string GroupName { get; set; } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Model/RegisterData.cs b/src/HopFrame.Web/Model/RegisterData.cs deleted file mode 100644 index 6d92531..0000000 --- a/src/HopFrame.Web/Model/RegisterData.cs +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/HopFrame.Web/Model/UserAdd.cs b/src/HopFrame.Web/Model/UserAdd.cs deleted file mode 100644 index e138395..0000000 --- a/src/HopFrame.Web/Model/UserAdd.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace HopFrame.Web.Model; - -internal sealed class UserAdd : RegisterData { - public string Group { get; set; } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor index e939548..9484d82 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor @@ -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 @@ - @foreach (var view in AdminMenu.Subpages) { - + @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { + - @view.Name - @view.Permission - @view.Description - Open + @adminPage.Title + @adminPage.Permissions.View + @adminPage.Description + Open @@ -31,3 +32,12 @@ @inject NavigationManager Navigator +@inject IAdminPagesProvider Pages + +@code { + + public void NavigateTo(string url) { + Navigator.NavigateTo("administration/" + url, true); + } + +} diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor new file mode 100644 index 0000000..c794eaf --- /dev/null +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -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 + +@_pageData.Title + + + + +
+

+ @_pageData.Title administration + + + +

+ + + + Add Entry + +
+ + + + + @foreach (var prop in GetListingProperties()) { + + @if (prop.Sortable) { + @prop.DisplayName + @if (_currentSortProperty == prop.Name) { + + } + } + else { + @prop.DisplayName + } + + } + + @if (_hasEditPermission || _hasDeletePermission) { + Actions + } + + + + + @foreach (var entry in _displayedModels) { + + @foreach (var prop in GetListingProperties()) { + @if (prop.Bold) { + + @GetPrintableValue(entry, prop) + + } + else { + + @GetPrintableValue(entry, prop) + + } + } + + @if (_hasEditPermission || _hasDeletePermission) { + + + @if (_hasEditPermission) { + Edit + } + + @if (_hasDeletePermission) { + Delete + } + + + } + + } + + + + + +@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 _modelBuffer; + private AdminPageModal _modal; + + private bool _hasEditPermission; + private bool _hasDeletePermission; + + private string _currentSortProperty; + private ListSortDirection _currentSortDirection; + private DateTime _lastSearch; + private IList _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 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.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 + }); + } + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor.css b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor.css similarity index 84% rename from src/HopFrame.Web/Pages/Administration/GroupsPage.razor.css rename to src/HopFrame.Web/Pages/Administration/AdminPageList.razor.css index 445d132..6f4b803 100644 --- a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor.css +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor.css @@ -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; -} diff --git a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor b/src/HopFrame.Web/Pages/Administration/GroupsPage.razor deleted file mode 100644 index 4591f60..0000000 --- a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor +++ /dev/null @@ -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 - -Groups - - - - -
-

- Groups administration - - - -

- - - - Add Group - -
- - - - - - Name - @if (_currentOrder == OrderType.Name) { - - } - - Description - Default - - Created - @if (_currentOrder == OrderType.Created) { - - } - - - @if (_hasEditPrivileges || _hasDeletePrivileges) { - Actions - } - - - - - @foreach (var group in _groups) { - - @group.Name.Replace("group.", "") - @group.Description - - @if (group.IsDefaultGroup) { - Yes - } - else { - No - } - - @group.CreatedAt - - @if (_hasEditPrivileges || _hasDeletePrivileges) { - - - @if (_hasEditPrivileges) { - Edit - } - - @if (_hasDeletePrivileges) { - Delete - } - - - } - - } - - - -@inject IGroupRepository Groups -@inject IPermissionRepository Permissions -@inject ITokenContext Auth -@inject SweetAlertService Alerts - -@code { - private IList _groups = new List(); - - 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(); - - _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 - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor index a66f311..ae96859 100644 --- a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor +++ b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor @@ -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 @@ Dashboard - @foreach (var nav in Subpages) { - - @nav.Name + @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { + + @adminPage.Title } @@ -46,25 +46,11 @@ @inject NavigationManager Navigator @inject ITokenContext Context @inject IAuthService Auth +@inject IAdminPagesProvider Pages @code { - public static IList Subpages = new List { - 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() { diff --git a/src/HopFrame.Web/Pages/Administration/UsersPage.razor b/src/HopFrame.Web/Pages/Administration/UsersPage.razor deleted file mode 100644 index 47b871c..0000000 --- a/src/HopFrame.Web/Pages/Administration/UsersPage.razor +++ /dev/null @@ -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 - -Users - - - - - -
-

- Users administration - - - -

- - - - Add User - -
- - - - - # - - E-Mail - @if (_currentOrder == OrderType.Email) { - - } - - - Username - @if (_currentOrder == OrderType.Username) { - - } - - - Registered - @if (_currentOrder == OrderType.Registered) { - - } - - Primary Group - - @if (_hasEditPrivileges || _hasDeletePrivileges) { - Actions - } - - - - - @foreach (var user in _users) { - - @user.Id - @user.Email - @user.Username - @user.CreatedAt - @GetFriendlyGroupName(user) - - @if (_hasEditPrivileges || _hasDeletePrivileges) { - - - @if (_hasEditPrivileges) { - Edit - } - - @if (_hasDeletePrivileges) { - Delete - } - - - } - - } - - - -@inject IUserRepository UserService -@inject IPermissionRepository PermissionsService -@inject IGroupRepository Groups -@inject SweetAlertService Alerts -@inject ITokenContext Auth - -@code { - private IList _users = new List(); - private IDictionary _userGroups = new Dictionary(); - - 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(); - _userGroups = new Dictionary(); - - _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 - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/UsersPage.razor.css b/src/HopFrame.Web/Pages/Administration/UsersPage.razor.css deleted file mode 100644 index 445d132..0000000 --- a/src/HopFrame.Web/Pages/Administration/UsersPage.razor.css +++ /dev/null @@ -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; -} diff --git a/src/HopFrame.Web/Repositories/GroupProvider.cs b/src/HopFrame.Web/Repositories/GroupProvider.cs new file mode 100644 index 0000000..953a9d7 --- /dev/null +++ b/src/HopFrame.Web/Repositories/GroupProvider.cs @@ -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 { + public override async Task> ReadAll() { + return await repo.GetPermissionGroups(); + } + + public override async Task Create(PermissionGroup model) { + return await repo.CreatePermissionGroup(model); + } + + public override async Task Update(PermissionGroup model) { + await repo.EditPermissionGroup(model); + return model; + } + + public override Task Delete(PermissionGroup model) { + return repo.DeletePermissionGroup(model); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Repositories/UserProvider.cs b/src/HopFrame.Web/Repositories/UserProvider.cs new file mode 100644 index 0000000..49ca30f --- /dev/null +++ b/src/HopFrame.Web/Repositories/UserProvider.cs @@ -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 { + public override async Task> ReadAll() { + return await repo.GetUsers(); + } + + public override Task Create(User model) { + return repo.AddUser(model); + } + + public override async Task Update(User model) { + await repo.UpdateUser(model); + return model; + } + + public override Task Delete(User model) { + return repo.DeleteUser(model); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index a9ab84e..548e2e9 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -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(); services.AddScoped(); services.AddTransient(); + services.AddAdminContext(); // Component library's services.AddSweetAlert2(); diff --git a/test/FrontendTest/AdminContext.cs b/test/FrontendTest/AdminContext.cs new file mode 100644 index 0000000..3c9af86 --- /dev/null +++ b/test/FrontendTest/AdminContext.cs @@ -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
Addresses { get; set; } + public AdminPage Employees { get; set; } + + public override void OnModelCreating(IAdminContextGenerator generator) { + base.OnModelCreating(generator); + + generator.Page() + .Property(e => e.Address) + .IsSelector(); + + generator.Page
() + .Property(a => a.Employee) + .Ignore(); + + generator.Page
() + .Property(a => a.AddressId) + .IsSelector() + .Parser((model, e) => model.AddressId = e.EmployeeId); + + generator.Page() + .ConfigureRepository() + .ListingProperty(e => e.Name); + + generator.Page
() + .ConfigureRepository() + .ListingProperty(a => a.City); + } +} \ No newline at end of file diff --git a/test/FrontendTest/Components/Pages/Counter.razor b/test/FrontendTest/Components/Pages/Counter.razor index 4fdbec5..4ac3989 100644 --- a/test/FrontendTest/Components/Pages/Counter.razor +++ b/test/FrontendTest/Components/Pages/Counter.razor @@ -1,4 +1,7 @@ @page "/counter" +@using System.Text.Json +@using HopFrame.Web +@using HopFrame.Web.Admin.Providers @rendermode InteractiveServer Counter @@ -9,12 +12,20 @@ +@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); } } \ No newline at end of file diff --git a/test/FrontendTest/DatabaseContext.cs b/test/FrontendTest/DatabaseContext.cs index 5da7d59..0dede8a 100644 --- a/test/FrontendTest/DatabaseContext.cs +++ b/test/FrontendTest/DatabaseContext.cs @@ -1,12 +1,24 @@ using HopFrame.Database; using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; namespace FrontendTest; public class DatabaseContext : HopDbContextBase { + public DbSet Employees { get; set; } + public DbSet
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() + .HasOne(e => e.Address) + .WithOne(a => a.Employee); } } \ No newline at end of file diff --git a/test/FrontendTest/FrontendTest.csproj b/test/FrontendTest/FrontendTest.csproj index 312aa4b..e043848 100644 --- a/test/FrontendTest/FrontendTest.csproj +++ b/test/FrontendTest/FrontendTest.csproj @@ -2,7 +2,7 @@ net8.0 - enable + disable enable diff --git a/test/FrontendTest/Models/Address.cs b/test/FrontendTest/Models/Address.cs new file mode 100644 index 0000000..386114d --- /dev/null +++ b/test/FrontendTest/Models/Address.cs @@ -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; } +} \ No newline at end of file diff --git a/test/FrontendTest/Models/Employee.cs b/test/FrontendTest/Models/Employee.cs new file mode 100644 index 0000000..6f70edc --- /dev/null +++ b/test/FrontendTest/Models/Employee.cs @@ -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; } +} \ No newline at end of file diff --git a/test/FrontendTest/Program.cs b/test/FrontendTest/Program.cs index af54f28..7547722 100644 --- a/test/FrontendTest/Program.cs +++ b/test/FrontendTest/Program.cs @@ -1,11 +1,13 @@ using FrontendTest; using FrontendTest.Components; using HopFrame.Web; +using HopFrame.Web.Admin; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(); builder.Services.AddHopFrame(); +builder.Services.AddAdminContext(); // Add services to the container. builder.Services.AddRazorComponents() diff --git a/test/FrontendTest/Providers/AddressProvider.cs b/test/FrontendTest/Providers/AddressProvider.cs new file mode 100644 index 0000000..94b203e --- /dev/null +++ b/test/FrontendTest/Providers/AddressProvider.cs @@ -0,0 +1,29 @@ +using HopFrame.Web.Admin; +using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; + +namespace FrontendTest.Providers; + +public class AddressProvider(DatabaseContext context) : ModelRepository
{ + + public override async Task> ReadAll() { + return await context.Addresses.ToArrayAsync(); + } + + public override async Task
Create(Address model) { + await context.Addresses.AddAsync(model); + await context.SaveChangesAsync(); + return model; + } + + public override async Task
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(); + } +} \ No newline at end of file diff --git a/test/FrontendTest/Providers/EmployeeProvider.cs b/test/FrontendTest/Providers/EmployeeProvider.cs new file mode 100644 index 0000000..0eb2aff --- /dev/null +++ b/test/FrontendTest/Providers/EmployeeProvider.cs @@ -0,0 +1,31 @@ +using HopFrame.Web.Admin; +using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; + +namespace FrontendTest.Providers; + +public class EmployeeProvider(DatabaseContext context) : ModelRepository { + + public override async Task> ReadAll() { + return await context.Employees + .Include(e => e.Address) + .ToArrayAsync(); + } + + public override async Task Create(Employee model) { + await context.Employees.AddAsync(model); + await context.SaveChangesAsync(); + return model; + } + + public override async Task 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(); + } +} \ No newline at end of file diff --git a/test/RestApiTest/DatabaseContext.cs b/test/RestApiTest/DatabaseContext.cs index 1167277..ef370c7 100644 --- a/test/RestApiTest/DatabaseContext.cs +++ b/test/RestApiTest/DatabaseContext.cs @@ -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) {