diff --git a/zero.Core/Configuration.cs b/zero.Core/Configuration.cs index 91e575bd..e5dad719 100644 --- a/zero.Core/Configuration.cs +++ b/zero.Core/Configuration.cs @@ -2,6 +2,9 @@ { public class BackofficeConfiguration : IBackofficeConfiguration { + public string DefaultLanguage { get; set; } + + public string SystemUserId { get; set; } public FoldersConfig Folders { get; set; } @@ -13,6 +16,9 @@ public interface IBackofficeConfiguration { + string DefaultLanguage { get; set; } + + string SystemUserId { get; set; } FoldersConfig Folders { get; set; } diff --git a/zero.Core/Entities/BackofficeUser/BackofficeUser.cs b/zero.Core/Entities/BackofficeUser/BackofficeUser.cs index 8892c41e..426170b7 100644 --- a/zero.Core/Entities/BackofficeUser/BackofficeUser.cs +++ b/zero.Core/Entities/BackofficeUser/BackofficeUser.cs @@ -29,7 +29,7 @@ namespace zero.Core.Entities public List RoleIds { get; set; } = new List(); /// - public List Claims { get; set; } = new List(); + public List Claims { get; set; } = new List(); @@ -105,7 +105,7 @@ namespace zero.Core.Entities /// /// The user's claims, for use in claims-based authentication. /// - List Claims { get; set; } + List Claims { get; set; } diff --git a/zero.Core/Entities/BackofficeUser/BackofficeUserClaim.cs b/zero.Core/Entities/BackofficeUser/BackofficeUserClaim.cs index 4a854332..957a53c7 100644 --- a/zero.Core/Entities/BackofficeUser/BackofficeUserClaim.cs +++ b/zero.Core/Entities/BackofficeUser/BackofficeUserClaim.cs @@ -14,7 +14,7 @@ namespace zero.Core.Entities public Claim ToClaim() => new Claim(Type, Value); /// - public IBackofficeUserClaim FromClaim(Claim other) + public BackofficeUserClaim FromClaim(Claim other) { Type = other?.Type; Value = other?.Value; @@ -39,7 +39,7 @@ namespace zero.Core.Entities /// /// Constructs a new claim with the type and value /// - IBackofficeUserClaim FromClaim(Claim other); + BackofficeUserClaim FromClaim(Claim other); /// /// Initializes by copying ClaimType and ClaimValue from the other claim diff --git a/zero.Core/Entities/BackofficeUser/BackofficeUserRole.cs b/zero.Core/Entities/BackofficeUser/BackofficeUserRole.cs index f86512ae..7d3c2574 100644 --- a/zero.Core/Entities/BackofficeUser/BackofficeUserRole.cs +++ b/zero.Core/Entities/BackofficeUser/BackofficeUserRole.cs @@ -11,7 +11,7 @@ namespace zero.Core.Entities public string Icon { get; set; } /// - public List Claims { get; set; } = new List(); + public List Claims { get; set; } = new List(); } @@ -30,6 +30,6 @@ namespace zero.Core.Entities /// /// The user's claims, for use in claims-based authentication. /// - List Claims { get; set; } + List Claims { get; set; } } } diff --git a/zero.Core/Entities/EntityChangeResult.cs b/zero.Core/Entities/EntityChangeResult.cs new file mode 100644 index 00000000..c5b5bf02 --- /dev/null +++ b/zero.Core/Entities/EntityChangeResult.cs @@ -0,0 +1,84 @@ +using FluentValidation.Results; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace zero.Core.Entities +{ + [DataContract(Name = "result", Namespace = "")] + public class EntityChangeResult + { + [DataMember(Name = "model")] + public T Model { get; set; } + + [DataMember(Name = "success")] + public bool IsSuccess { get; set; } + + [DataMember(Name = "errors")] + public IList Errors { get; set; } = new List(); + + public EntityChangeResult() { } + + public EntityChangeResult(ValidationResult validation) + { + IsSuccess = validation.IsValid; + Errors = validation.Errors.Select(x => new EntityChangeResultError() + { + Property = x.PropertyName, + Message = x.ErrorMessage + }).ToList(); + } + + public void AddError(string property, string message) + { + IsSuccess = false; + Errors.Add(new EntityChangeResultError() + { + Property = property, + Message = message + }); + } + + + public EntityChangeResult Combine(EntityChangeResult with) + { + if (IsSuccess && !with.IsSuccess) + { + IsSuccess = false; + } + foreach (EntityChangeResultError error in with.Errors) + { + Errors.Add(error); + } + return this; + } + + + public static EntityChangeResult Success() => new EntityChangeResult() { IsSuccess = true }; + + public static EntityChangeResult Success(T model) => new EntityChangeResult() { IsSuccess = true, Model = model }; + + public static EntityChangeResult Fail() => new EntityChangeResult() { }; + + public static EntityChangeResult Fail(string property, string message) + { + EntityChangeResult result = new EntityChangeResult(); + result.AddError(property, message); + return result; + } + + public static EntityChangeResult Fail(ValidationResult validation) => new EntityChangeResult(validation); + + public static EntityChangeResult Fail(EntityChangeResultError error) => Fail(error.Property, error.Message); + } + + [DataContract(Name = "resultError", Namespace = "")] + public class EntityChangeResultError + { + [DataMember(Name = "property")] + public string Property { get; set; } + + [DataMember(Name = "message")] + public string Message { get; set; } + } +} diff --git a/zero.Core/Extensions/ValidatorExtensions.cs b/zero.Core/Extensions/ValidatorExtensions.cs new file mode 100644 index 00000000..26c27e6f --- /dev/null +++ b/zero.Core/Extensions/ValidatorExtensions.cs @@ -0,0 +1,81 @@ +using FluentValidation; +using System; +using System.Linq; + +namespace zero.Core.Extensions +{ + public static class ValidatorExtensions + { + private const char DOT = '.'; + + private const char KLAMMERAFFE = '@'; + + private static string HEX_REGEX = "#[0-9a-fA-F]{3,8}"; + + /// + /// Validate a color input as HEX (#aabbccdd or #aabbcc or #abc) + /// + public static IRuleBuilderOptions Hex(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Matches(HEX_REGEX).WithMessage("@errors/format_hex"); + } + + + /// + /// Validate an email + /// + 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/invalid_mail"); + } + + + /// + /// Validate one or multiple emails + /// + public static IRuleBuilderOptions Emails(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must((root, value, context) => + { + if (value.IsNullOrWhiteSpace()) + { + return true; + } + + string[] mails = value.Split(',', ';').Select(x => x.Trim()).Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + + if (!mails.Any()) + { + return false; + } + + foreach (string mail in mails) + { + int index = value.IndexOf(KLAMMERAFFE); + + if (index < 0 || index == value.Length - 1 || index != value.LastIndexOf(KLAMMERAFFE)) + { + return false; + } + } + + return true; + }).WithMessage("@errors/invalid_mails"); + } + } +} diff --git a/zero.Core/Identity/BackofficeUserStore.cs b/zero.Core/Identity/BackofficeUserStore.cs new file mode 100644 index 00000000..6979ddaf --- /dev/null +++ b/zero.Core/Identity/BackofficeUserStore.cs @@ -0,0 +1,61 @@ +using FluentValidation.Results; +using Raven.Client.Documents; +using System; +using System.Threading; +using System.Threading.Tasks; +using zero.Core.Entities; +using zero.Core.Validation; +using zero.Core.Extensions; + +namespace zero.Core.Identity +{ + public class BackofficeUserStore + where TUser : IBackofficeUser, new() + where TRole : IBackofficeUserRole, new() + { + /// + /// Gets the database context for this store. + /// + protected IDocumentStore Raven { get; set; } + + /// + /// Configuration + /// + protected IBackofficeConfiguration Config { get; private set; } + + /// + /// Currently logged-in user + /// + protected IBackofficeUser Current { get; private set; } + + + public BackofficeUserStore(IDocumentStore raven, IBackofficeConfiguration config, IBackofficeUser currentUser) + { + Raven = raven; + Config = config; + } + + + /// + /// Adds a new backoffice user + /// + public async Task> Add(TUser model, CancellationToken token = default) + { + // set default language + if (model.LanguageId.IsNullOrEmpty()) + { + model.LanguageId = Config.DefaultLanguage; + } + + // validate model + ValidationResult validation = await new BackofficeUserValidator(isCreate: true).ValidateAsync(model); + + if (!validation.IsValid) + { + return EntityChangeResult.Fail(validation); + } + + return EntityChangeResult.Success(); + } + } +} diff --git a/zero.Core/Validation/BackofficeUserValidator.cs b/zero.Core/Validation/BackofficeUserValidator.cs new file mode 100644 index 00000000..96083ebf --- /dev/null +++ b/zero.Core/Validation/BackofficeUserValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using zero.Core.Entities; +using zero.Core.Extensions; + +namespace zero.Core.Validation +{ + public class BackofficeUserValidator : AbstractValidator + { + public BackofficeUserValidator(bool isCreate = false) + { + RuleFor(x => x.Name).NotEmpty(); + RuleFor(x => x.Email).Email(); + RuleFor(x => x.PasswordHash).NotEmpty(); + RuleFor(x => x.LanguageId).NotEmpty(); // TODO only allow available languages + RuleFor(x => x.RoleIds).NotEmpty(); + + if (isCreate) + { + RuleFor(x => x.IsSuper).Equals(false); + } + } + } +} diff --git a/zero.Core/zero.Core.csproj b/zero.Core/zero.Core.csproj index a32b3b1a..67402917 100644 --- a/zero.Core/zero.Core.csproj +++ b/zero.Core/zero.Core.csproj @@ -6,8 +6,10 @@ + + \ No newline at end of file