From d38cce6dc2c6d261fccd1e57e9bcf9f7758a375f Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sun, 27 Oct 2024 15:26:25 +0100 Subject: [PATCH] Added validation to admin pages --- HopFrame.sln.DotSettings.user | 2 + src/HopFrame.Database/Models/User.cs | 2 +- .../Members/AdminUniqueAttribute.cs | 4 ++ .../Generators/IAdminPropertyGenerator.cs | 3 +- .../Implementation/AdminPropertyGenerator.cs | 12 +++++- .../Models/AdminPageProperty.cs | 4 +- .../Administration/AdminPageModal.razor | 43 ++++++++++++++----- src/HopFrame.Web/HopAdminContext.cs | 12 +++++- 8 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 src/HopFrame.Web.Admin/Attributes/Members/AdminUniqueAttribute.cs diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index baebe71..a38eed3 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -1,5 +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/src/HopFrame.Database/Models/User.cs b/src/HopFrame.Database/Models/User.cs index 2b73010..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] 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/Generators/IAdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs index 720b103..3d35d52 100644 --- a/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs @@ -11,11 +11,12 @@ public interface IAdminPropertyGenerator { IAdminPropertyGenerator Ignore(bool ignore = true); IAdminPropertyGenerator Generated(bool generated = true); IAdminPropertyGenerator Bold(bool bold = true); + IAdminPropertyGenerator Unique(bool unique = true); IAdminPropertyGenerator DisplayName(string displayName); IAdminPropertyGenerator Description(string description); IAdminPropertyGenerator Prefix(string prefix); - IAdminPropertyGenerator Validator(Func validator); + IAdminPropertyGenerator Validator(Func validator); IAdminPropertyGenerator IsSelector(); IAdminPropertyGenerator Parser(Func parser); IAdminPropertyGenerator ParserForListType(Func parser); diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs index 3cb2368..9673508 100644 --- a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs @@ -51,6 +51,11 @@ internal sealed class AdminPropertyGenerator(string name, Type type) return this; } + public IAdminPropertyGenerator Unique(bool unique = true) { + _property.Unique = unique; + return this; + } + public IAdminPropertyGenerator DisplayName(string displayName) { _property.DisplayName = displayName; return this; @@ -66,8 +71,8 @@ internal sealed class AdminPropertyGenerator(string name, Type type) return this; } - public IAdminPropertyGenerator Validator(Func validator) { - _property.Validator = validator; + public IAdminPropertyGenerator Validator(Func validator) { + _property.Validator = o => validator.Invoke((TProperty)o); return this; } @@ -115,6 +120,9 @@ internal sealed class AdminPropertyGenerator(string name, Type type) 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; diff --git a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs index 95eadc4..2ae5497 100644 --- a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs +++ b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs @@ -17,12 +17,14 @@ public sealed class AdminPageProperty { public bool Bold { get; set; } public bool Required { get; set; } public bool Ignore { get; set; } + public bool Unique { get; set; } + [JsonIgnore] public Type Type { get; set; } public Type SelectorType { get; set; } - public Func Validator { get; set; } + public Func Validator { get; set; } public Func Parser { get; set; } public object GetValue(object entry) { diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor index 5c05d68..6a97503 100644 --- a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor +++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor @@ -26,14 +26,14 @@ @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})) { + @foreach (var element in GetListPropertyValues(prop).Select((e, i) => new { e, i })) { @@ -77,12 +77,17 @@ else if (prop.Prefix is not null && !_isEdit) { @prop.Prefix - + } else { @prop.DisplayName - + + @if (_validation[_validationIdentifiers[prop]].Any()) { +
+ @_validation[_validationIdentifiers[prop]].First() +
+ } }
} @@ -102,11 +107,15 @@ @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 IModelRepository _repository; @@ -126,11 +135,14 @@ _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(); @@ -217,11 +229,23 @@ } private void Validate(object sender, ValidationRequestedEventArgs e) { + _validation.Clear(); foreach (var value in _values) { + if (value.Key.Unique) { + 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; - if (value.Key.Validator?.Invoke(value.Value) == true) continue; - Console.WriteLine("INVALID"); - //TODO: implement validation + var error = value.Key.Validator?.Invoke(value.Value); + if (string.IsNullOrEmpty(error)) continue; + _validation.Add(_validationIdentifiers[value.Key], error); } } @@ -274,7 +298,7 @@ if (!_isEdit) { await _repository.CreateO(_entry); - await Alerts.FireAsync(new SweetAlertOptions { + Alerts.FireAsync(new SweetAlertOptions { Title = "New entry added!", Icon = SweetAlertIcon.Success, ShowConfirmButton = false, @@ -284,12 +308,11 @@ else { await _repository.UpdateO(_entry); - await Alerts.FireAsync(new SweetAlertOptions { + Alerts.FireAsync(new SweetAlertOptions { Title = "Entry updated!", Icon = SweetAlertIcon.Success, ShowConfirmButton = false, Timer = 1500 - }); } diff --git a/src/HopFrame.Web/HopAdminContext.cs b/src/HopFrame.Web/HopAdminContext.cs index 84a87a5..68fdd90 100644 --- a/src/HopFrame.Web/HopAdminContext.cs +++ b/src/HopFrame.Web/HopAdminContext.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using HopFrame.Database.Models; using HopFrame.Security; using HopFrame.Web.Admin; @@ -23,7 +24,16 @@ public class HopAdminContext : AdminPagesContext { generator.Page().Property(u => u.Password) .DisplayInListing(false) - .DisplayValueWhileEditing(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);