From 430de2ae54d4a4717c5c9be92e96686561b80a6a Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Mon, 12 Dec 2022 15:06:29 +0100 Subject: [PATCH] fix identity stuff + add validator extensions + localizer updates --- zero/Configuration/Constants.cs | 1 - zero/Identity/ZeroIdentityConstants.cs | 4 +- zero/Identity/ZeroIdentityExtensions.cs | 2 +- zero/Identity/ZeroRoleStore(TRole).cs | 29 ++-------- zero/Identity/ZeroUserStore(TUser).cs | 16 +----- zero/Identity/ZeroUserStore(TUser,TRole).cs | 4 +- zero/Localization/ConfigurationLocalizer.cs | 1 - zero/Localization/CultureResolver.cs | 2 - zero/Models/ZeroEntity.cs | 10 +--- zero/Mvc/ZeroMvcModule.cs | 15 +++++ zero/Mvc/ZeroPageModel.cs | 20 +++++++ zero/Utils/ObjectCopycat.cs | 10 ++-- .../ModelStateDictionaryExtensions.cs | 56 +++++++++++++++++++ zero/Validation/ValidatorExtensions.cs | 40 ++++++------- zero/ZeroBuilder.cs | 2 + zero/zero.csproj | 1 - 16 files changed, 133 insertions(+), 80 deletions(-) create mode 100644 zero/Mvc/ZeroMvcModule.cs create mode 100644 zero/Mvc/ZeroPageModel.cs create mode 100644 zero/Validation/ModelStateDictionaryExtensions.cs diff --git a/zero/Configuration/Constants.cs b/zero/Configuration/Constants.cs index d9b6832d..96d42cbc 100644 --- a/zero/Configuration/Constants.cs +++ b/zero/Configuration/Constants.cs @@ -17,6 +17,5 @@ public static partial class Constants public static partial class Database { public const string ReservationPrefix = "zero."; - public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires; } } diff --git a/zero/Identity/ZeroIdentityConstants.cs b/zero/Identity/ZeroIdentityConstants.cs index 28962812..9effa54b 100644 --- a/zero/Identity/ZeroIdentityConstants.cs +++ b/zero/Identity/ZeroIdentityConstants.cs @@ -21,7 +21,9 @@ public class ZeroIdentityConstants public static readonly string IsZero = ClaimPrefix + ".iszero"; public static readonly string UserId = ClaimPrefix + ".userid"; - public static readonly string UserName = ClaimPrefix + ".username"; + public static readonly string Username = ClaimPrefix + ".username"; + public static readonly string Name = ClaimPrefix + ".name"; + public static readonly string Nickname = ClaimPrefix + ".nickname"; public static readonly string Role = ClaimPrefix + ".rolealias"; public static readonly string SecurityStamp = ClaimPrefix + ".securitystamp"; public static readonly string Email = ClaimPrefix + ".email"; diff --git a/zero/Identity/ZeroIdentityExtensions.cs b/zero/Identity/ZeroIdentityExtensions.cs index 4cdbf5c3..e207ac77 100644 --- a/zero/Identity/ZeroIdentityExtensions.cs +++ b/zero/Identity/ZeroIdentityExtensions.cs @@ -106,7 +106,7 @@ public static class ZeroIdentityExtensions services.Configure(opts => { opts.ClaimsIdentity.UserIdClaimType = ZeroIdentityConstants.Claims.UserId; - opts.ClaimsIdentity.UserNameClaimType = ZeroIdentityConstants.Claims.UserName; + opts.ClaimsIdentity.UserNameClaimType = ZeroIdentityConstants.Claims.Username; opts.ClaimsIdentity.RoleClaimType = ZeroIdentityConstants.Claims.Role; opts.ClaimsIdentity.SecurityStampClaimType = ZeroIdentityConstants.Claims.SecurityStamp; opts.ClaimsIdentity.EmailClaimType = ZeroIdentityConstants.Claims.Email; diff --git a/zero/Identity/ZeroRoleStore(TRole).cs b/zero/Identity/ZeroRoleStore(TRole).cs index 4bf6f5c8..2c760592 100644 --- a/zero/Identity/ZeroRoleStore(TRole).cs +++ b/zero/Identity/ZeroRoleStore(TRole).cs @@ -1,7 +1,4 @@ using Microsoft.AspNetCore.Identity; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using Raven.Client.Exceptions; using System.Security.Claims; using Microsoft.AspNetCore.Mvc.ApplicationModels; using zero.Identity; @@ -71,18 +68,11 @@ public class ZeroRoleStore : /// public async Task UpdateAsync(TRole role, CancellationToken cancellationToken) { - try - { - Result result = await Db.Update(role); + Result result = await Db.Update(role); - if (!result.IsSuccess) - { - return Fail(result); - } - } - catch (ConcurrencyException) + if (!result.IsSuccess) { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + return Fail(result); } return IdentityResult.Success; } @@ -91,18 +81,11 @@ public class ZeroRoleStore : /// public async Task DeleteAsync(TRole role, CancellationToken cancellationToken) { - try - { - Result result = await Db.Delete(role); + Result result = await Db.Delete(role); - if (!result.IsSuccess) - { - return Fail(result); - } - } - catch (ConcurrencyException) + if (!result.IsSuccess) { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + return Fail(result); } return IdentityResult.Success; } diff --git a/zero/Identity/ZeroUserStore(TUser).cs b/zero/Identity/ZeroUserStore(TUser).cs index 2602b03e..a1fe0641 100644 --- a/zero/Identity/ZeroUserStore(TUser).cs +++ b/zero/Identity/ZeroUserStore(TUser).cs @@ -1,10 +1,7 @@ using FluentValidation.Results; using Microsoft.AspNetCore.Identity; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; using System.Security.Claims; using System.Security.Cryptography; -using Raven.Client.Exceptions; using zero.Identity; namespace zero.Identity; @@ -94,18 +91,11 @@ public partial class ZeroUserStore : /// public async Task DeleteAsync(TUser user, CancellationToken cancellationToken) { - try - { - Result result = await Db.Delete(user); + Result result = await Db.Delete(user); - if (!result.IsSuccess) - { - return Fail(result); - } - } - catch (ConcurrencyException) + if (!result.IsSuccess) { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + return Fail(result); } return IdentityResult.Success; } diff --git a/zero/Identity/ZeroUserStore(TUser,TRole).cs b/zero/Identity/ZeroUserStore(TUser,TRole).cs index 7106b437..b1d2c749 100644 --- a/zero/Identity/ZeroUserStore(TUser,TRole).cs +++ b/zero/Identity/ZeroUserStore(TUser,TRole).cs @@ -1,6 +1,4 @@ using Microsoft.AspNetCore.Identity; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; using zero.Identity; namespace zero.Identity; @@ -31,7 +29,7 @@ public partial class ZeroUserStore : ZeroUserStore, /// public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) { - return await Db.FindAll(x => roleName.In(x.RoleIds), cancellationToken); + return await Db.FindAll(x => x.RoleIds.Contains(roleName), cancellationToken); } diff --git a/zero/Localization/ConfigurationLocalizer.cs b/zero/Localization/ConfigurationLocalizer.cs index bc085460..c9379fe0 100644 --- a/zero/Localization/ConfigurationLocalizer.cs +++ b/zero/Localization/ConfigurationLocalizer.cs @@ -1,6 +1,5 @@ using System.IO; using Microsoft.AspNetCore.Hosting; -using Newtonsoft.Json; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; diff --git a/zero/Localization/CultureResolver.cs b/zero/Localization/CultureResolver.cs index a75c750c..c612145a 100644 --- a/zero/Localization/CultureResolver.cs +++ b/zero/Localization/CultureResolver.cs @@ -1,7 +1,5 @@ using FluentValidation; using Microsoft.Extensions.Logging; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; using System.Globalization; using System.Security.Claims; diff --git a/zero/Models/ZeroEntity.cs b/zero/Models/ZeroEntity.cs index 9883681f..193ce720 100644 --- a/zero/Models/ZeroEntity.cs +++ b/zero/Models/ZeroEntity.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using System.Diagnostics; +using System.Diagnostics; namespace zero.Models; @@ -65,12 +64,5 @@ public class ZeroEntity : ZeroIdEntity, ISupportsDbConventions, ISupportsRouting /// Additional properties for this entity /// public Dictionary Properties { get; set; } = new(); - - /// - /// [Warning] This field is always empty when bound to the database. - /// It is only filled in the app-code for routing. - /// - [JsonIgnore] - public string Url { get; set; } } \ No newline at end of file diff --git a/zero/Mvc/ZeroMvcModule.cs b/zero/Mvc/ZeroMvcModule.cs new file mode 100644 index 00000000..c79da7e4 --- /dev/null +++ b/zero/Mvc/ZeroMvcModule.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mvc; + +public class ZeroMvcModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddRouting(opts => opts.LowercaseUrls = true); + } +} \ No newline at end of file diff --git a/zero/Mvc/ZeroPageModel.cs b/zero/Mvc/ZeroPageModel.cs new file mode 100644 index 00000000..30ec39bb --- /dev/null +++ b/zero/Mvc/ZeroPageModel.cs @@ -0,0 +1,20 @@ +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mvc; + +public abstract class ZeroPageModel : PageModel +{ + /// + /// Get access to the zero context for this request + /// + public IZeroContext ZeroContext => _zeroContext ?? (_zeroContext = HttpContext?.RequestServices?.GetService()); + IZeroContext _zeroContext; + + /// + /// Get access to the localizer + /// + public ILocalizer Localizer => _localizer ?? (_localizer = HttpContext?.RequestServices?.GetService()); + ILocalizer _localizer; +} \ No newline at end of file diff --git a/zero/Utils/ObjectCopycat.cs b/zero/Utils/ObjectCopycat.cs index cf13f02d..4e37dcde 100644 --- a/zero/Utils/ObjectCopycat.cs +++ b/zero/Utils/ObjectCopycat.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Reflection; +using System.Text.Json; namespace zero.Utils; @@ -16,20 +16,20 @@ public class ObjectCopycat public static T Clone(T obj) { Type type = obj.GetType(); - return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type); + return (T)JsonSerializer.Deserialize(JsonSerializer.Serialize(obj), type); } public static T CloneSpecific(T obj) { Type type = obj.GetType(); - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj)); + return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj)); } public static bool ContentEquals(T obj1, T obj2) { - return (obj1 == null && obj2 == null) || (obj1 != null && obj2 != null && JsonConvert.SerializeObject(obj1) == JsonConvert.SerializeObject(obj2)); + return (obj1 == null && obj2 == null) || (obj1 != null && obj2 != null && JsonSerializer.Serialize(obj1) == JsonSerializer.Serialize(obj2)); } diff --git a/zero/Validation/ModelStateDictionaryExtensions.cs b/zero/Validation/ModelStateDictionaryExtensions.cs new file mode 100644 index 00000000..2e840c94 --- /dev/null +++ b/zero/Validation/ModelStateDictionaryExtensions.cs @@ -0,0 +1,56 @@ +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace zero.Validation; + +public static class ModelStateDictionaryExtensions +{ + const char DOT = '.'; + + public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState, bool convertUnresolvedPropertiesToModel = true, string prefix = "Form", Dictionary aliases = default, IEnumerable removePrefixes = default) + { + if (result == null || result.IsValid) + { + return; + } + + string[] formProperties = modelState.Keys.ToArray(); + + foreach (var error in result.Errors) + { + string propertyName = error.PropertyName; + + if (propertyName != null && removePrefixes != null && removePrefixes.Any()) + { + foreach (var removePrefix in removePrefixes) + { + if (propertyName.StartsWith(removePrefix, StringComparison.InvariantCultureIgnoreCase)) + { + propertyName = propertyName.Substring(removePrefix.Length); + break; + } + } + } + + string key = prefix.IsNullOrEmpty() ? propertyName : (propertyName.IsNullOrEmpty() ? String.Empty : prefix + DOT + propertyName); + string message = error.ErrorMessage; + bool found = formProperties.Contains(key, StringComparer.InvariantCultureIgnoreCase); + + // find property by alias + if (!found && aliases != null && aliases.ContainsKey(propertyName)) + { + key = aliases[propertyName]; + key = prefix.IsNullOrEmpty() ? key : (key.IsNullOrEmpty() ? String.Empty : prefix + DOT + key); + found = formProperties.Contains(key, StringComparer.InvariantCultureIgnoreCase); + } + + // move property errors to model if they are not part of the form + if (!found && convertUnresolvedPropertiesToModel) + { + key = String.Empty; + } + + modelState.AddModelError(key, message); + } + } +} \ No newline at end of file diff --git a/zero/Validation/ValidatorExtensions.cs b/zero/Validation/ValidatorExtensions.cs index 0e9e4a7e..187b18f6 100644 --- a/zero/Validation/ValidatorExtensions.cs +++ b/zero/Validation/ValidatorExtensions.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Raven.Client.Documents; using System.Globalization; +using System.Net.Mail; namespace zero.Validation; @@ -38,22 +38,7 @@ public static class ValidatorExtensions /// public static IRuleBuilderOptions Email(this IRuleBuilder ruleBuilder) { - return ruleBuilder.Must((root, value, context) => - { - if (value.IsNullOrWhiteSpace()) - { - return true; - } - - int index = value.IndexOf(KLAMMERAFFE); - - if (index < 0 || index == value.Length - 1 || index != value.LastIndexOf(KLAMMERAFFE)) - { - return false; - } - - return true; - }).WithMessage("@errors.forms.email_invalid"); + return ruleBuilder.Must((root, value, context) => ValidateEmail(value)).WithMessage("@errors.forms.email_invalid"); } @@ -78,9 +63,7 @@ public static class ValidatorExtensions foreach (string mail in mails) { - int index = mail.IndexOf(KLAMMERAFFE); - - if (index < 0 || index == mail.Length - 1 || index != mail.LastIndexOf(KLAMMERAFFE)) + if (!ValidateEmail(mail)) { return false; } @@ -91,6 +74,23 @@ public static class ValidatorExtensions } + private static bool ValidateEmail(string value) + { + if (value == null) + { + return false; + } + + int index = value.IndexOf(KLAMMERAFFE); + + return + index > 0 && + index != value.Length - 1 && + index == value.LastIndexOf(KLAMMERAFFE) && + MailAddress.TryCreate(value, out _); + } + + /// /// Validates a culture identifier /// diff --git a/zero/ZeroBuilder.cs b/zero/ZeroBuilder.cs index 7edf5d29..805e67ca 100644 --- a/zero/ZeroBuilder.cs +++ b/zero/ZeroBuilder.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using zero.Mvc; namespace zero; @@ -42,6 +43,7 @@ public class ZeroBuilder new AssemblyDiscovery(Mvc).Execute(_startupOptions.AssemblyDiscoveryRules); Modules.Add(); + Modules.Add(); Modules.Add(); Modules.Add(); Modules.Add(); diff --git a/zero/zero.csproj b/zero/zero.csproj index 96ead0c2..6f7056a9 100644 --- a/zero/zero.csproj +++ b/zero/zero.csproj @@ -16,7 +16,6 @@ -