Added validation to admin pages

This commit is contained in:
2024-10-27 15:26:25 +01:00
parent 85a45ece55
commit d38cce6dc2
8 changed files with 66 additions and 16 deletions

View File

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

View File

@@ -8,7 +8,7 @@ public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required, MinLength(36), MaxLength(36)]
public Guid Id { get; init; } public Guid Id { get; init; }
[MaxLength(50)] [Required, MaxLength(50)]
public string Username { get; set; } public string Username { get; set; }
[Required, MaxLength(50), EmailAddress] [Required, MaxLength(50), EmailAddress]

View File

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

View File

@@ -11,11 +11,12 @@ public interface IAdminPropertyGenerator<TProperty> {
IAdminPropertyGenerator<TProperty> Ignore(bool ignore = true); IAdminPropertyGenerator<TProperty> Ignore(bool ignore = true);
IAdminPropertyGenerator<TProperty> Generated(bool generated = true); IAdminPropertyGenerator<TProperty> Generated(bool generated = true);
IAdminPropertyGenerator<TProperty> Bold(bool bold = true); IAdminPropertyGenerator<TProperty> Bold(bool bold = true);
IAdminPropertyGenerator<TProperty> Unique(bool unique = true);
IAdminPropertyGenerator<TProperty> DisplayName(string displayName); IAdminPropertyGenerator<TProperty> DisplayName(string displayName);
IAdminPropertyGenerator<TProperty> Description(string description); IAdminPropertyGenerator<TProperty> Description(string description);
IAdminPropertyGenerator<TProperty> Prefix(string prefix); IAdminPropertyGenerator<TProperty> Prefix(string prefix);
IAdminPropertyGenerator<TProperty> Validator(Func<object, bool> validator); IAdminPropertyGenerator<TProperty> Validator(Func<TProperty, string> validator);
IAdminPropertyGenerator<TProperty> IsSelector<TSelector>(); IAdminPropertyGenerator<TProperty> IsSelector<TSelector>();
IAdminPropertyGenerator<TProperty> Parser<TModel>(Func<TModel, string, TProperty> parser); IAdminPropertyGenerator<TProperty> Parser<TModel>(Func<TModel, string, TProperty> parser);
IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty>(Func<TModel, string, TInnerProperty> parser); IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty>(Func<TModel, string, TInnerProperty> parser);

View File

@@ -51,6 +51,11 @@ internal sealed class AdminPropertyGenerator<TProperty>(string name, Type type)
return this; return this;
} }
public IAdminPropertyGenerator<TProperty> Unique(bool unique = true) {
_property.Unique = unique;
return this;
}
public IAdminPropertyGenerator<TProperty> DisplayName(string displayName) { public IAdminPropertyGenerator<TProperty> DisplayName(string displayName) {
_property.DisplayName = displayName; _property.DisplayName = displayName;
return this; return this;
@@ -66,8 +71,8 @@ internal sealed class AdminPropertyGenerator<TProperty>(string name, Type type)
return this; return this;
} }
public IAdminPropertyGenerator<TProperty> Validator(Func<object, bool> validator) { public IAdminPropertyGenerator<TProperty> Validator(Func<TProperty, string> validator) {
_property.Validator = validator; _property.Validator = o => validator.Invoke((TProperty)o);
return this; return this;
} }
@@ -116,6 +121,9 @@ internal sealed class AdminPropertyGenerator<TProperty>(string name, Type type)
if (attributes.Any(a => a is AdminUneditableAttribute)) if (attributes.Any(a => a is AdminUneditableAttribute))
Editable(false); Editable(false);
if (attributes.Any(a => a is AdminUniqueAttribute))
Unique();
if (attributes.Any(a => a is AdminIgnoreAttribute)) { if (attributes.Any(a => a is AdminIgnoreAttribute)) {
var attribute = attributes.Single(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute; var attribute = attributes.Single(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute;
DisplayInListing(false); DisplayInListing(false);

View File

@@ -17,12 +17,14 @@ public sealed class AdminPageProperty {
public bool Bold { get; set; } public bool Bold { get; set; }
public bool Required { get; set; } public bool Required { get; set; }
public bool Ignore { get; set; } public bool Ignore { get; set; }
public bool Unique { get; set; }
[JsonIgnore] [JsonIgnore]
public Type Type { get; set; } public Type Type { get; set; }
public Type SelectorType { get; set; } public Type SelectorType { get; set; }
public Func<object, bool> Validator { get; set; } public Func<object, string> Validator { get; set; }
public Func<object, string, object> Parser { get; set; } public Func<object, string, object> Parser { get; set; }
public object GetValue(object entry) { public object GetValue(object entry) {

View File

@@ -77,12 +77,17 @@
else if (prop.Prefix is not null && !_isEdit) { else if (prop.Prefix is not null && !_isEdit) {
<BSInputGroup> <BSInputGroup>
<span class="@BS.Input_Group_Text">@prop.Prefix</span> <span class="@BS.Input_Group_Text">@prop.Prefix</span>
<input type="text" class="form-control" disabled="@IsDisabled(prop)" required="@IsRequired(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = prop.Prefix + e.Value"/> <input type="text" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = prop.Prefix + e.Value"/>
</BSInputGroup> </BSInputGroup>
} }
else { else {
<BSLabel>@prop.DisplayName</BSLabel> <BSLabel>@prop.DisplayName</BSLabel>
<input type="@GetInputType(prop)" class="form-control" disabled="@IsDisabled(prop)" required="@IsRequired(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/> <input type="@GetInputType(prop)" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/>
@if (_validation[_validationIdentifiers[prop]].Any()) {
<div class="invalid-feedback" style="display: block">
@_validation[_validationIdentifiers[prop]].First()
</div>
}
} }
</div> </div>
} }
@@ -102,11 +107,15 @@
@inject ITokenContext Auth @inject ITokenContext Auth
@code { @code {
#pragma warning disable CS4014
[Parameter] [Parameter]
public Func<Task> ReloadDelegate { get; set; } public Func<Task> ReloadDelegate { get; set; }
private BSModalBase _modal; private BSModalBase _modal;
private EditContext _context; private EditContext _context;
private ValidationMessageStore _validation;
private Dictionary<AdminPageProperty, FieldIdentifier> _validationIdentifiers;
private IDictionary<AdminPageProperty, object> _values; private IDictionary<AdminPageProperty, object> _values;
private IModelRepository _repository; private IModelRepository _repository;
@@ -126,11 +135,14 @@
_entry ??= Activator.CreateInstance(_currentPage.ModelType); _entry ??= Activator.CreateInstance(_currentPage.ModelType);
_context = new EditContext(_entry); _context = new EditContext(_entry);
_validation = new ValidationMessageStore(_context);
_validationIdentifiers = new Dictionary<AdminPageProperty, FieldIdentifier>();
_context.OnValidationRequested += Validate; _context.OnValidationRequested += Validate;
_values = new Dictionary<AdminPageProperty, object>(); _values = new Dictionary<AdminPageProperty, object>();
foreach (var property in _currentPage.Properties) { foreach (var property in _currentPage.Properties) {
_values.Add(property, property.GetValue(_entry)); _values.Add(property, property.GetValue(_entry));
_validationIdentifiers.Add(property, new FieldIdentifier(_entry, property.Name));
} }
await _modal.ShowAsync(); await _modal.ShowAsync();
@@ -217,11 +229,23 @@
} }
private void Validate(object sender, ValidationRequestedEventArgs e) { private void Validate(object sender, ValidationRequestedEventArgs e) {
_validation.Clear();
foreach (var value in _values) { 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 is null) continue;
if (value.Key.Validator?.Invoke(value.Value) == true) continue; var error = value.Key.Validator?.Invoke(value.Value);
Console.WriteLine("INVALID"); if (string.IsNullOrEmpty(error)) continue;
//TODO: implement validation _validation.Add(_validationIdentifiers[value.Key], error);
} }
} }
@@ -274,7 +298,7 @@
if (!_isEdit) { if (!_isEdit) {
await _repository.CreateO(_entry); await _repository.CreateO(_entry);
await Alerts.FireAsync(new SweetAlertOptions { Alerts.FireAsync(new SweetAlertOptions {
Title = "New entry added!", Title = "New entry added!",
Icon = SweetAlertIcon.Success, Icon = SweetAlertIcon.Success,
ShowConfirmButton = false, ShowConfirmButton = false,
@@ -284,12 +308,11 @@
else { else {
await _repository.UpdateO(_entry); await _repository.UpdateO(_entry);
await Alerts.FireAsync(new SweetAlertOptions { Alerts.FireAsync(new SweetAlertOptions {
Title = "Entry updated!", Title = "Entry updated!",
Icon = SweetAlertIcon.Success, Icon = SweetAlertIcon.Success,
ShowConfirmButton = false, ShowConfirmButton = false,
Timer = 1500 Timer = 1500
}); });
} }

View File

@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security; using HopFrame.Security;
using HopFrame.Web.Admin; using HopFrame.Web.Admin;
@@ -23,7 +24,16 @@ public class HopAdminContext : AdminPagesContext {
generator.Page<User>().Property(u => u.Password) generator.Page<User>().Property(u => u.Password)
.DisplayInListing(false) .DisplayInListing(false)
.DisplayValueWhileEditing(false); .DisplayValueWhileEditing(false)
.Validator(passwd => passwd.Length >= 8 ? null : "The password needs to be at least 8 characters long!");
generator.Page<User>().Property(u => u.Email)
.Validator(email => Regex.Match(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").Success ? null : "Invalid E-Mail address!")
.Unique();
generator.Page<User>().Property(u => u.Username)
.Validator(uname => uname.Length >= 4 ? null : "The username needs to be at least 4 characters long!")
.Unique();
generator.Page<User>().Property(u => u.CreatedAt) generator.Page<User>().Property(u => u.CreatedAt)
.Editable(false); .Editable(false);