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">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD;

View File

@@ -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]

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> Generated(bool generated = true);
IAdminPropertyGenerator<TProperty> Bold(bool bold = true);
IAdminPropertyGenerator<TProperty> Unique(bool unique = true);
IAdminPropertyGenerator<TProperty> DisplayName(string displayName);
IAdminPropertyGenerator<TProperty> Description(string description);
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> Parser<TModel>(Func<TModel, string, TProperty> 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;
}
public IAdminPropertyGenerator<TProperty> Unique(bool unique = true) {
_property.Unique = unique;
return this;
}
public IAdminPropertyGenerator<TProperty> DisplayName(string displayName) {
_property.DisplayName = displayName;
return this;
@@ -66,8 +71,8 @@ internal sealed class AdminPropertyGenerator<TProperty>(string name, Type type)
return this;
}
public IAdminPropertyGenerator<TProperty> Validator(Func<object, bool> validator) {
_property.Validator = validator;
public IAdminPropertyGenerator<TProperty> Validator(Func<TProperty, string> validator) {
_property.Validator = o => validator.Invoke((TProperty)o);
return this;
}
@@ -116,6 +121,9 @@ internal sealed class AdminPropertyGenerator<TProperty>(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;
DisplayInListing(false);

View File

@@ -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<object, bool> Validator { get; set; }
public Func<object, string> Validator { get; set; }
public Func<object, string, object> Parser { get; set; }
public object GetValue(object entry) {

View File

@@ -77,12 +77,17 @@
else if (prop.Prefix is not null && !_isEdit) {
<BSInputGroup>
<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>
}
else {
<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>
}
@@ -102,11 +107,15 @@
@inject ITokenContext Auth
@code {
#pragma warning disable CS4014
[Parameter]
public Func<Task> ReloadDelegate { get; set; }
private BSModalBase _modal;
private EditContext _context;
private ValidationMessageStore _validation;
private Dictionary<AdminPageProperty, FieldIdentifier> _validationIdentifiers;
private IDictionary<AdminPageProperty, object> _values;
private IModelRepository _repository;
@@ -126,11 +135,14 @@
_entry ??= Activator.CreateInstance(_currentPage.ModelType);
_context = new EditContext(_entry);
_validation = new ValidationMessageStore(_context);
_validationIdentifiers = new Dictionary<AdminPageProperty, FieldIdentifier>();
_context.OnValidationRequested += Validate;
_values = new Dictionary<AdminPageProperty, object>();
foreach (var property in _currentPage.Properties) {
_values.Add(property, property.GetValue(_entry));
_validationIdentifiers.Add(property, new FieldIdentifier(_entry, property.Name));
}
await _modal.ShowAsync();
@@ -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
});
}

View File

@@ -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<User>().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<User>().Property(u => u.Email)
.Validator(email => Regex.Match(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").Success ? null : "Invalid E-Mail address!")
.Unique();
generator.Page<User>().Property(u => u.Username)
.Validator(uname => uname.Length >= 4 ? null : "The username needs to be at least 4 characters long!")
.Unique();
generator.Page<User>().Property(u => u.CreatedAt)
.Editable(false);