diff --git a/old/zero.Core/Api/Safenames.cs b/old/zero.Core/Api/Safenames.cs deleted file mode 100644 index 1da0deea..00000000 --- a/old/zero.Core/Api/Safenames.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.IO; -using System.Text; -using zero.Core.Extensions; - -namespace zero.Core.Api -{ - public class Safenames - { - public enum Scope - { - Url, - File - } - - const char HYPHEN = '-'; - - const char DOT = '.'; - - const char PLUS = '+'; - - const char AMPERSAND = '&'; - - - /// - /// Converts an untrusted to a safe filename - /// - public static string File(string value) - { - return Generate(Path.GetFileName(value), Scope.File); - } - - - /// - /// Converts a term to a safe alias (suitable for URLs) - /// - public static string Alias(string value) - { - return Generate(value, Scope.Url); - } - - - /// - /// Converts a term to a safe alias (suitable for URLs) - /// - public static string Alias(object value) - { - return Generate(value?.ToString(), Scope.Url); - } - - - /// - /// - /// - static string Generate(string value, Scope scope) - { - if (String.IsNullOrWhiteSpace(value)) - { - return String.Empty; - } - - char previous = default; - StringBuilder output = new StringBuilder(); - - for (int i = 0; i < value.Length; i++) - { - // get character in lower case - char character = char.ToLower(value[i]); - char target; - - // do not handle surrogates - if (char.IsSurrogate(character)) - { - continue; - } - - // special replacements accents + umlauts - if (character.TryReplaceAccent(out char[] replacement)) - { - if (replacement.Length > 1) - { - output.Append(replacement); - output.Remove(output.Length - 1, 1); - } - target = replacement[replacement.Length - 1]; - } - // append character a-z, 0-9 - else if (character.IsAZor09()) - { - target = character; - } - // + sign for + and & - else if (character == PLUS || character == AMPERSAND) - { - target = PLUS; - } - else if (scope == Scope.File && character == DOT) - { - target = DOT; - } - // add hyphen for all other characters - else - { - target = HYPHEN; - } - - // add default characters - if (target != HYPHEN && target != PLUS) - { - output.Append(target); - } - // add hyphen if it isn't first and previous char is not + or - - else if (target == HYPHEN && previous != default && previous != PLUS && previous != HYPHEN) - { - output.Append(target); - } - // add plus. do remove hyphen it is the previous character - else if (target == PLUS) - { - if (previous == HYPHEN) - { - output.Remove(output.Length - 1, 1); - } - output.Append(target); - } - - if (output.Length > 0) - { - previous = output[output.Length - 1]; - } - } - - if (output.Length > 0 && !output[output.Length - 1].IsAZor09()) - { - output.Remove(output.Length - 1, 1); - } - - if (output.Length == 0) - { - output.Append(HYPHEN); - } - - return output.ToString(); - } - } -} \ No newline at end of file diff --git a/old/zero.Core/Api/Token.cs b/old/zero.Core/Api/Token.cs deleted file mode 100644 index 5b8797b3..00000000 --- a/old/zero.Core/Api/Token.cs +++ /dev/null @@ -1,111 +0,0 @@ -//using Raven.Client.Documents; -//using Raven.Client.Documents.Session; -//using System; -//using System.Linq; -//using System.Threading.Tasks; -//using zero.Core.Database; -//using zero.Core.Entities; -//using zero.Core.Extensions; -//using zero.Core.Options; - -//namespace zero.Core.Api -//{ -// public class Token : IToken -// { -// protected IZeroStore Store { get; private set; } - -// protected IZeroOptions Options { get; private set; } - -// private const string PREFIX = "changeTokens"; - - -// public Token(IZeroStore store, IZeroOptions options) -// { -// Store = store; -// Options = options; -// } - - -// /// -// public bool Verify(ZeroIdEntity entity, string token) -// { -// return Verify(entity?.Id, token); -// } - - -// /// -// public bool Verify(string entityId, string token) -// { -// if (token.IsNullOrWhiteSpace() && entityId.IsNullOrEmpty()) -// { -// return true; -// } - -// if (token.IsNullOrWhiteSpace() || entityId.IsNullOrEmpty()) -// { -// return false; -// } - -// using (IDocumentSession session = Store.OpenSession()) -// { -// return session.Query().Any(x => x.Id == token && x.ReferenceId == entityId); -// } -// } - - -// /// -// public string Get(ZeroIdEntity entity) -// { -// return Get(entity?.Id); -// } - - -// /// -// public string Get(string entityId) -// { -// if (entityId.IsNullOrEmpty()) -// { -// return null; -// } - -// ChangeToken token = new ChangeToken() -// { -// Id = Options.Raven.CollectionPrefix.EnsureEndsWith(Store.Raven.Conventions.IdentityPartsSeparator) + PREFIX.EnsureEndsWith(Store.Raven.Conventions.IdentityPartsSeparator) + Guid.NewGuid(), -// ReferenceId = entityId -// }; - -// using (IDocumentSession session = Store.OpenSession()) -// { -// session.Store(token); -// session.Advanced.GetMetadataFor(token)[Constants.Database.Expires] = DateTime.UtcNow.AddMinutes(Options.TokenExpiration); -// session.SaveChanges(); -// } - -// return token.Id; -// } -// } - - -// public interface IToken -// { -// /// -// /// Verifies if the change token is valid for the entity -// /// -// bool Verify(ZeroIdEntity entity, string token); - -// /// -// /// Verifies if the change token is valid for the entity -// /// -// bool Verify(string entityId, string token); - -// /// -// /// Get a new change token for the entity -// /// -// string Get(ZeroIdEntity entity); - -// /// -// /// Get a new change token for the entity -// /// -// string Get(string entityId); -// } -//} diff --git a/old/zero.Core/ApplicationResolver.cs b/old/zero.Core/ApplicationResolver.cs deleted file mode 100644 index a0c17e4d..00000000 --- a/old/zero.Core/ApplicationResolver.cs +++ /dev/null @@ -1,240 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Handlers; -using zero.Core.Identity; -using zero.Core.Options; - -namespace zero.Core -{ - public class ApplicationResolver : IApplicationResolver - { - protected IZeroStore Store { get; private set; } - - protected IZeroOptions Options { get; private set; } - - protected ILogger Logger { get; private set; } - - protected IHandlerHolder Handler { get; private set; } - - private IList Apps { get; set; } - - - - public ApplicationResolver(IZeroStore store, IZeroOptions options, ILogger logger, IHandlerHolder handler = null) - { - Store = store; - Options = options; - Logger = logger; - Handler = handler; - } - - - /// - public async Task Resolve(HttpContext context, ClaimsPrincipal user) - { - if (context?.Request == null) - { - Logger.LogWarning("Could not resolve application as HTTP request is null"); - return null; - } - - Application app; - - if (context.IsBackofficeRequest(Options.BackofficePath)) - { - app = await ResolveFromUser(user); - } - else - { - app = await ResolveFromRequest(context); - } - - if (app == null) - { - //Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host); - IList apps = await GetApplications(); - app = apps.FirstOrDefault(); - } - - return app; - } - - - /// - public async Task ResolveFromUser(ClaimsPrincipal user) - { - BackofficeUser userEntity = await GetBackofficeUser(user); - return await ResolveFromUser(userEntity); - } - - - /// - public async Task ResolveFromUser(BackofficeUser user) - { - if (user == null) - { - return null; - } - - string appId; - string[] allowedAppIds = user.GetAllowedAppIds(); - - if (!user.CurrentAppId.IsNullOrEmpty()) - { - if (user.IsSuper || allowedAppIds.Contains(user.CurrentAppId, StringComparer.InvariantCultureIgnoreCase)) - { - appId = user.CurrentAppId; - } - else - { - appId = user.AppId; - } - } - else - { - appId = user.AppId; - } - - if (appId.IsNullOrEmpty()) - { - throw new Exception($"User entity ${user.Id} needs a valid AppId"); - } - - IAsyncDocumentSession session = Store.Session(global: true); - return await session.LoadAsync(appId); - } - - - /// - public async Task ResolveFromRequest(HttpContext context) - { - IApplicationResolverHandler handler = Handler.Get(); - Application app = handler?.Resolve(context.Request, await GetApplications()); - - if (app == null) - { - app = await ResolveFromUri(context.Request.GetEncodedUrl()); - } - - return app; - } - - - /// - public async Task ResolveFromUri(string uriString) - { - return ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications()); - } - - - /// - public async Task ResolveFromUri(Uri uri) - { - return ResolveFromUriInternal(uri, await GetApplications()); - } - - - /// - /// Get matching application from an URI - /// - Application ResolveFromUriInternal(Uri uri, IList apps) - { - foreach (Application app in apps) - { - if (app.Domains?.Length < 1) - { - Logger.LogWarning("No domains specified for app {app}", app.Id); - continue; - } - - foreach (Uri domain in app.Domains) - { - int compareResult = Uri.Compare(uri, domain, UriComponents.HostAndPort, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase); - if (compareResult == 0) - { - return app; - } - } - } - - return null; - } - - - /// - /// Get all applications to choose from - /// - async Task> GetApplications() - { - if (Apps != null) - { - return Apps; - } - - IAsyncDocumentSession session = Store.Session(global: true); - Apps = await session.Query().ToListAsync(); - return Apps; - } - - - /// - /// Get backoffice user from claims principal - /// - async Task GetBackofficeUser(ClaimsPrincipal user) - { - string userId = user.FindFirstValue(Constants.Auth.Claims.UserId); - - IAsyncDocumentSession session = Store.Session(global: true); - return await session.LoadAsync(userId); - } - } - - - public interface IApplicationResolver - { - /// - /// Resolves the current application from either the backoffice user (in case it is backoffice request) - /// or the domain (in case it is frontend request). - /// - Task Resolve(HttpContext context, ClaimsPrincipal user); - - /// - /// Resolves the current application from the request path - /// - Task ResolveFromRequest(HttpContext context); - - /// - /// Get matching application from an URI string - /// - Task ResolveFromUri(string uriString); - - /// - /// Get matching application from an URI - /// - Task ResolveFromUri(Uri uri); - - /// - /// Resolves the current application from the logged-in backoffice user. - /// This method won't return apps the user has no access to. - /// - Task ResolveFromUser(ClaimsPrincipal user); - - /// - /// Resolves the current application from a user. - /// This method won't return apps the user has no access to. - /// - Task ResolveFromUser(BackofficeUser user); - } -} diff --git a/old/zero.Core/Attributes/GenerateIdAttribute.cs b/old/zero.Core/Attributes/GenerateIdAttribute.cs deleted file mode 100644 index 1f771ff5..00000000 --- a/old/zero.Core/Attributes/GenerateIdAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace zero.Core.Attributes -{ - /// - /// Automatically generate ID with the specified length and insert it into this property on entity save - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class GenerateIdAttribute : Attribute - { - public int? Length = null; - - public GenerateIdAttribute(int length) - { - Length = length; - } - - public GenerateIdAttribute() { } - } -} diff --git a/old/zero.Core/Attributes/LocalizeAttribute.cs b/old/zero.Core/Attributes/LocalizeAttribute.cs deleted file mode 100644 index 494bddbc..00000000 --- a/old/zero.Core/Attributes/LocalizeAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace zero.Core.Attributes -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = false)] - public class LocalizeAttribute : Attribute - { - public string Key; - - public LocalizeAttribute(string key) - { - Key = key; - } - } -} diff --git a/old/zero.Core/Attributes/OperationCancelledExceptionFilterAttribute.cs b/old/zero.Core/Attributes/OperationCancelledExceptionFilterAttribute.cs deleted file mode 100644 index 202e6d31..00000000 --- a/old/zero.Core/Attributes/OperationCancelledExceptionFilterAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; -using System; - -namespace zero.Core.Attributes -{ - public class OperationCancelledExceptionFilterAttribute : ExceptionFilterAttribute - { - private readonly ILogger _logger; - - - public OperationCancelledExceptionFilterAttribute(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - - public override void OnException(ExceptionContext context) - { - if (context.Exception is OperationCanceledException) - { - _logger.LogInformation("Request was cancelled"); - context.ExceptionHandled = true; - context.Result = new StatusCodeResult(400); - } - } - } -} diff --git a/old/zero.Core/Blueprints/Blueprint.cs b/old/zero.Core/Blueprints/Blueprint.cs deleted file mode 100644 index 4a448556..00000000 --- a/old/zero.Core/Blueprints/Blueprint.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Utils; - -namespace zero.Core.Blueprints -{ - /// - /// - /// - public class Blueprint : Blueprint where T : ZeroEntity, new() - { - public List> UnlockedFields { get; private set; } = new(); - - /// - /// These fields are not copied by the ObjectCloner - /// as they are either locked or copied manually by ApplyDefaults() - /// - protected static string[] DefaultUncopyFields { get; set; } = new[] { "id", "url", "name", "alias", "key", "sort", "isActive", "hash", "languageId", "createdById", "createdDate", "lastModifiedById", "lastModifiedDate", "blueprint" }; - - - public Blueprint() : base(typeof(T)) { } - - - /// - /// Merges blueprint data into a model based on the configuration - /// - public virtual T Apply(T blueprint, T model) - { - // reset blueprint options for model in case no blueprint was found - if (blueprint == null || model.Blueprint == null) - { - model.Blueprint = null; - return model; - } - - // copy all properties which are synced from the blueprint to the model - ApplyDefaults(blueprint, model); - ObjectCloner.CopyProperties(blueprint, model, DefaultUncopyFields.Union(model.Blueprint.Desync).ToArray()); - - return model; - } - - - /// - public override ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model) => Apply(blueprint as T, model as T); - - - /// - /// Update default entity data - /// - protected virtual void ApplyDefaults(T blueprint, T model) - { - if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Name), StringComparer)) - { - model.Name = blueprint.Name; - model.Alias = blueprint.Alias; - } - if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Key), StringComparer)) - { - model.Key = blueprint.Key; - } - if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Sort), StringComparer)) - { - model.Sort = blueprint.Sort; - } - if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.IsActive), StringComparer)) - { - model.IsActive = blueprint.IsActive; - } - - // these values can't be overridden - model.Hash = blueprint.Hash; - model.LanguageId = blueprint.LanguageId; - model.CreatedById = blueprint.CreatedById; - model.CreatedDate = blueprint.CreatedDate; - model.LastModifiedById = blueprint.LastModifiedById; - model.LastModifiedDate = blueprint.LastModifiedDate; - } - - - /// - /// Lock a field so it always get synchronized no and cannot be changed - /// - public void LockAll() - { - UnlockedFields = new(); - } - - - /// - /// Lock a field so it always get synchronized no and cannot be changed - /// - public void Lock(Expression> selector) - { - RemoveField(selector); - } - - - public void Unlock(Expression> selector) - { - AddField(selector); - } - - - public void UnlockDefaults() - { - AddField(x => x.Name); - AddField(x => x.Alias); - AddField(x => x.Sort); - AddField(x => x.Key); - AddField(x => x.IsActive); - } - - - /// - public override IEnumerable GetUnlockedFieldNames() - { - foreach (BlueprintField field in UnlockedFields) - { - yield return field.FieldName.ToCamelCaseId(); - } - } - - - /// - /// Get existing field or create a new one - /// - protected BlueprintField AddField(Expression> selector) - { - BlueprintField tempField = new(selector); - BlueprintField storedField = UnlockedFields.FirstOrDefault(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase)); - - if (storedField != null) - { - return storedField; - } - - UnlockedFields.Add(tempField); - return tempField; - } - - - /// - /// Removes a field - /// - protected void RemoveField(Expression> selector) - { - BlueprintField tempField = new(selector); - - if (tempField != null) - { - UnlockedFields.RemoveAll(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase)); - } - } - } - - - /// - /// - /// - public abstract class Blueprint - { - /// - /// Type of the associated entity - /// - public Type ContentType { get; private set; } - - public string Alias { get; private set; } - - /// - /// String comparer for property name comparison - /// - protected readonly StringComparer StringComparer = StringComparer.InvariantCultureIgnoreCase; - - - public Blueprint(Type type) - { - ContentType = type; - Alias = type.Name.ToCamelCaseId(); - } - - /// - /// Merges blueprint data into a model based on the configuration - /// - public abstract ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model); - - /// - /// Get all fields which have been unlocked - /// - public abstract IEnumerable GetUnlockedFieldNames(); - } - - - public class DefaultBlueprint : Blueprint where T : ZeroEntity, new() - { - public DefaultBlueprint(Action> expression = null) : base() - { - expression?.Invoke(this); - } - } -} diff --git a/old/zero.Core/Blueprints/BlueprintChildInterceptor.cs b/old/zero.Core/Blueprints/BlueprintChildInterceptor.cs deleted file mode 100644 index 29e95118..00000000 --- a/old/zero.Core/Blueprints/BlueprintChildInterceptor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Database; -using zero.Core.Entities; - -namespace zero.Core.Blueprints -{ - public class BlueprintChildInterceptor : CollectionInterceptor - { - protected IZeroContext Context { get; set; } - - protected IZeroStore Store { get; set; } - - protected ILogger Logger { get; set; } - - protected IBlueprintService BlueprintService { get; set; } - - - public BlueprintChildInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService) - { - Context = context; - Store = store; - Logger = logger; - BlueprintService = blueprintService; - } - - /// - public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) - { - // this interceptor does only work for child entities - return args.Context.Store.ResolvedDatabase != Context.Options.Raven.Database; - } - - - /// - public override Task> Deleting(InterceptorParameters args, ZeroIdEntity model) - { - if (model is not ZeroEntity || (model as ZeroEntity).Blueprint != null) - { - InterceptorResult result = new(); - result.Result = EntityResult.Fail("@blueprint.errors.cannotDeleteChild"); - return Task.FromResult(result); - } - - return base.Deleting(args, model); - } - } -} diff --git a/old/zero.Core/Blueprints/BlueprintField.cs b/old/zero.Core/Blueprints/BlueprintField.cs deleted file mode 100644 index d2ea2b01..00000000 --- a/old/zero.Core/Blueprints/BlueprintField.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Blueprints -{ - public class BlueprintField where T : ZeroEntity - { - public Expression> Expression { get; private set; } - - public PropertyInfo PropertyInfo { get; private set; } - - public string FieldName { get; private set; } - - Action _applyHook { get; set; } - - Func _selectorFunc = null; - - - internal BlueprintField(Expression> selector) - { - Expression = selector; - FieldName = selector.GetPropertyPath(out MemberExpression member); - PropertyInfo = member?.Member as PropertyInfo; - - if (PropertyInfo == null) - { - throw new ArgumentException("selector parameter has to be a property selector"); - } - - if (FieldName.IsNullOrEmpty()) - { - throw new Exception("Could not find a matching field name"); - } - } - - - public virtual void Apply(T blueprint, T model) - { - if (_applyHook != null) - { - _applyHook(blueprint, model); - return; - } - - _selectorFunc ??= Expression.Compile(); - PropertyInfo.SetValue(model, _selectorFunc(blueprint)); - } - - - public virtual void OnUpdate(Action onUpdate) - { - _applyHook = onUpdate; - } - } -} diff --git a/old/zero.Core/Blueprints/BlueprintInterceptor.cs b/old/zero.Core/Blueprints/BlueprintInterceptor.cs deleted file mode 100644 index 43d883f3..00000000 --- a/old/zero.Core/Blueprints/BlueprintInterceptor.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.Extensions.Logging; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Blueprints -{ - public class BlueprintInterceptor : CollectionInterceptor - { - protected IZeroContext Context { get; set; } - - protected IZeroStore Store { get; set; } - - protected ILogger Logger { get; set; } - - protected IBlueprintService BlueprintService { get; set; } - - IList Apps { get; set; } - - - public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService) - { - Context = context; - Store = store; - Logger = logger; - BlueprintService = blueprintService; - } - - /// - public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) - { - // we do only update children if we operate on the shared database - return args.Context.Store.ResolvedDatabase == Context.Options.Raven.Database; - } - - - /// - public override async Task Saved(InterceptorParameters args, ZeroIdEntity model) - { - if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) - { - return; - } - - ZeroEntity entity = model as ZeroEntity; - int count = 0; - - foreach (Application app in await GetApplications()) - { - using ZeroContextScope scope = Context.CreateScope(app); - IZeroDocumentSession session = scope.Store.Session(scope.Database); - - ZeroEntity child = await session.LoadAsync(model.Id); - - if (child == null) - { - child = entity.Clone(); - child.Blueprint = new() { Id = model.Id }; - } - else - { - blueprint.Apply(entity, child); - } - - count += 1; - - // now we have to store the child - // but this will not work with the session as we need to run the scoped interceptors, - // therefore we need access to the collection - - await session.StoreAsync(child); - await session.SaveChangesAsync(); - } - - Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, entity.Name, model.Id); - } - - - /// - public override async Task Deleted(InterceptorParameters args, ZeroIdEntity model) - { - if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) - { - return; - } - - ZeroEntity entity = model as ZeroEntity; - int count = 0; - - foreach (Application app in await GetApplications()) - { - using ZeroContextScope scope = Context.CreateScope(app); - IZeroDocumentSession session = scope.Store.Session(scope.Database); - - count += 1; - - // now we have to delete the child - // but this will not work with the session as we need to run the scoped interceptors, - // therefore we need access to the collection - session.Delete(entity.Id); - await session.SaveChangesAsync(); - } - - Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, entity.Name, entity.Id); - } - - - /// - /// Get all applications to choose from - /// - async Task> GetApplications() - { - if (Apps != null) - { - return Apps; - } - - IAsyncDocumentSession session = Store.Session(global: true); - Apps = await session.Query().ToListAsync(); - return Apps; - } - - - - - /// - //public override async Task Saved(SaveParameters args) - //{ - - // //Logger.LogInformation("Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})", countRoutes - countUpdatedRoutes, countUpdatedRoutes, obsoleteRoutes.Count, args.Model.Name, args.Model.Id); - //} - } -} diff --git a/old/zero.Core/Blueprints/BlueprintService.cs b/old/zero.Core/Blueprints/BlueprintService.cs deleted file mode 100644 index 58c241dd..00000000 --- a/old/zero.Core/Blueprints/BlueprintService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Options; - -namespace zero.Core.Blueprints -{ - public class BlueprintService : IBlueprintService - { - protected IZeroOptions Options { get; set; } - - protected IReadOnlyCollection Blueprints { get; set; } - - - public BlueprintService(IZeroOptions options) - { - Options = options; - Blueprints = Options.Blueprints.GetAllItems(); - } - - - /// - public bool IsEnabled(T model) => IsEnabled(model.GetType()); - - - /// - public bool IsEnabled()=> IsEnabled(typeof(T)); - - - /// - public bool IsEnabled(Type type) => Blueprints.Any(x => x.ContentType.IsAssignableFrom(type)); - - - /// - public bool TryGetBlueprint(T model, out Blueprint blueprint) => TryGetBlueprint(model.GetType(), out blueprint); - - - /// - public bool TryGetBlueprint(out Blueprint blueprint) => TryGetBlueprint(typeof(T), out blueprint); - - - /// - public bool TryGetBlueprint(Type type, out Blueprint blueprint) - { - blueprint = Blueprints.FirstOrDefault(x => x.ContentType.IsAssignableFrom(type)); - return blueprint != null; - } - } - - - public interface IBlueprintService - { - /// - /// Check whether blueprinting functionality is enabled for a certain entity - /// - bool IsEnabled(); - - /// - /// Check whether blueprinting functionality is enabled for a certain entity - /// - bool IsEnabled(T model); - - bool IsEnabled(Type type); - - bool TryGetBlueprint(T model, out Blueprint blueprint); - - bool TryGetBlueprint(out Blueprint blueprint); - - bool TryGetBlueprint(Type type, out Blueprint blueprint); - } -} diff --git a/old/zero.Core/Collections/Interceptors/CollectionInterceptor.cs b/old/zero.Core/Collections/Interceptors/CollectionInterceptor.cs deleted file mode 100644 index c5d72504..00000000 --- a/old/zero.Core/Collections/Interceptors/CollectionInterceptor.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Collections -{ - public abstract partial class CollectionInterceptor : ICollectionInterceptor where T : ZeroIdEntity - { - /// - public virtual bool CanRun(InterceptorParameters args, T model) => true; - - /// - public virtual Task> Creating(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Updating(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Saving(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Deleting(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Saved(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask; - } - - - public abstract partial class CollectionInterceptor : CollectionInterceptor, ICollectionInterceptor - { - /// - public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) => base.CanRun(args, model); - } - - public interface ICollectionInterceptor : ICollectionInterceptor { } - - public interface ICollectionInterceptor where T : ZeroIdEntity - { - /// - /// Whether any of the interceptor methods is allowed to run based on the parameters - /// - bool CanRun(InterceptorParameters args, T model); - - /// - /// Called after an entity has been stored but before the session has saved its changes - /// - Task Created(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Creating(InterceptorParameters args, T model); - - /// - /// Called after an entity has been deleted but before the session has saved its changes - /// - Task Deleted(InterceptorParameters args, T model); - - /// - /// Called before an entity is deleted - /// - Task> Deleting(InterceptorParameters args, T model); - - /// - /// Called after an entity has been updated but before the session has saved its changes - /// - Task Updated(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Updating(InterceptorParameters args, T model); - - /// - /// Called after an entity has been saved (created or updated) but before the session has saved its changes - /// - Task Saved(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Saving(InterceptorParameters args, T model); - } -} diff --git a/old/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs b/old/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs deleted file mode 100644 index c244c429..00000000 --- a/old/zero.Core/Collections/Interceptors/CollectionInterceptorShim.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Collections -{ - public sealed class CollectionInterceptorShim : CollectionInterceptor where T : ZeroIdEntity - { - ICollectionInterceptor _base; - - public CollectionInterceptorShim(ICollectionInterceptor baseInterceptor) - { - _base = baseInterceptor; - } - - InterceptorResult Result(InterceptorResult result) - { - return result == null ? null : new InterceptorResult() - { - InterceptorHash = result.InterceptorHash, - Parameters = result.Parameters, - Prevent = result.Prevent, - Result = result.Result != null ? EntityResult.From(result.Result, result.Result.Model as T) : null - }; - } - - /// - public override bool CanRun(InterceptorParameters args, T model) => _base.CanRun(args, model); - - /// - public override async Task> Creating(InterceptorParameters args, T model) => Result(await _base.Creating(args, model)); - - /// - public override async Task> Updating(InterceptorParameters args, T model) => Result(await _base.Updating(args, model)); - - /// - public override async Task> Saving(InterceptorParameters args, T model) => Result(await _base.Saving(args, model)); - - /// - public override async Task> Deleting(InterceptorParameters args, T model) => Result(await _base.Deleting(args, model)); - - /// - public override Task Created(InterceptorParameters args, T model) => _base.Created(args, model); - - /// - public override Task Updated(InterceptorParameters args, T model) => _base.Updated(args, model); - - /// - public override Task Saved(InterceptorParameters args, T model) => _base.Saved(args, model); - - /// - public override Task Deleted(InterceptorParameters args, T model) => _base.Deleted(args, model); - } -} diff --git a/old/zero.Core/Collections/Interceptors/InterceptorResult.cs b/old/zero.Core/Collections/Interceptors/InterceptorResult.cs deleted file mode 100644 index 30023eaf..00000000 --- a/old/zero.Core/Collections/Interceptors/InterceptorResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using zero.Core.Entities; - -namespace zero.Core.Collections -{ - public class InterceptorResult - { - /// - /// Autoset. Hash used to match interceptors in order to correctly pass parameters - /// - internal string InterceptorHash { get; set; } - - /// - /// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...) - /// - public bool Prevent { get; set; } - - /// - /// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors - /// - public EntityResult Result { get; set; } - - /// - /// Additional parameters which can be passed to the interceptor after the operation was completed - /// - public Dictionary Parameters { get; set; } = new(); - } -} diff --git a/old/zero.Core/Constants.cs b/old/zero.Core/Constants.cs deleted file mode 100644 index 21128a8b..00000000 --- a/old/zero.Core/Constants.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace zero.Core -{ - public static class Constants - { - public const string ErrorFieldNone = "__zero_no_field"; - - public static class Tabs - { - public const string General = "general"; - } - - public static class Auth - { - public const string SystemUser = "system"; - public const string DefaultScheme = "zeroScheme"; - public const string BackofficeDisplayName = "Zero Bacckoffice"; - public const string BackofficeScheme = "zeroBackoffice"; - public const string BackofficeCookieName = "zero.be.session"; - public const string DefaultCookieName = "zero.session"; - - public static class Claims - { - public const string IsZero = "zero.claim.iszero"; - public const string IsSuper = "zero.claim.issuper"; - public const string UserId = "zero.claim.userid"; - public const string UserName = "zero.claim.username"; - public const string Role = "zero.claim.rolealias"; - public const string SecurityStamp = "zero.claim.securitystamp"; - public const string Email = "zero.claim.email"; - public const string Permission = "zero.claim.permission"; - public const string DefaultAppId = "zero.claim.defaultAppId"; - public const string AppId = "zero.claim.appId"; - public const string TicketExpires = "zero.claim.ticketExpires"; - } - } - - public static class Database - { - public const string ReservationPrefix = "zero."; - public const string CoreIdPrefix = "core."; - public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires; - } - - public static class Sections - { - public const string Dashboard = "dashboard"; - public const string Pages = "pages"; - public const string Spaces = "spaces"; - public const string Media = "media"; - public const string Settings = "settings"; - } - - public static class Settings - { - public const string Updates = "updates"; - public const string Applications = "applications"; - public const string Users = "users"; - public const string Languages = "languages"; - public const string Translations = "translations"; - public const string Countries = "countries"; - public const string Mails = "mailTemplates"; - public const string Integrations = "integrations"; - public const string Logging = "logs"; - public const string Plugins = "plugins"; - public const string CreatePlugin = "createplugin"; - } - - public static class PermissionCollections - { - public const string Sections = "permissionCollectionSections"; - public const string Spaces = "permissionCollectionSpaces"; - public const string Settings = "permisssionCollectionSettings"; - public const string Modules = "permissionCollectionModules"; - } - - public static class Pages - { - public const string FolderAlias = "zero.folder"; - public const string DefaultRootPageTypeAlias = "root"; - public const string PageRouteProviderAlias = "zero.pages"; - } - - public static class Routing - { - public const string InternalRoutePrefix = "/__zero/"; - - public const string ErrorRoute = InternalRoutePrefix + "error"; - } - } -} diff --git a/old/zero.Core/Cultures/CultureResolver.cs b/old/zero.Core/Cultures/CultureResolver.cs deleted file mode 100644 index a0b7b9a9..00000000 --- a/old/zero.Core/Cultures/CultureResolver.cs +++ /dev/null @@ -1,78 +0,0 @@ -using FluentValidation; -using Microsoft.Extensions.Logging; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System; -using System.Globalization; -using System.Threading.Tasks; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Cultures -{ - public class CultureResolver : ICultureResolver - { - protected ILogger Logger { get; private set; } - - - public CultureResolver(ILogger logger) - { - Logger = logger; - } - - - /// - public async Task Resolve(IZeroContext context) - { - // TODO this is just fake, we need to correctly resolve culture here - - if (context.IsBackofficeRequest) - { - - } - else - { - var session = context.Store.Session(); - Language language = await session.Query().FirstOrDefaultAsync(); - - if (language == null) - { - Logger.LogWarning("Could not set request culture as there is no available Language stored"); - return CultureInfo.CurrentCulture; - } - - try - { - CultureInfo culture = CultureInfo.CreateSpecificCulture(language.Code); - - if (culture.ThreeLetterISOLanguageName.IsNullOrEmpty()) - { - throw new Exception("ThreeLetterISOLanguageName is empty"); - } - - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = culture; - ValidatorOptions.Global.LanguageManager.Culture = culture; - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not create culture from Language code {code}", language.Code); - return CultureInfo.CurrentCulture; - } - } - - return CultureInfo.CurrentCulture; - } - } - - - public interface ICultureResolver - { - /// - /// Resolves the current application from either the backoffice user (in case it is backoffice request) - /// or the domain (in case it is frontend request). - /// - Task Resolve(IZeroContext context); - } -} diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_Countries.cs b/old/zero.Core/Database/Indexes/Backoffice/zero_Countries.cs deleted file mode 100644 index ea6b043b..00000000 --- a/old/zero.Core/Database/Indexes/Backoffice/zero_Countries.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class zero_Countries : ZeroIndex - { - protected override void Create() - { - Map = items => items.Select(item => new - { - Name = item.Name, - IsPreferred = item.IsPreferred - }); - - Index(x => x.Name, FieldIndexing.Search); - } - } -} diff --git a/old/zero.Core/Database/Indexes/MediaFolder_ByHierarchy.cs b/old/zero.Core/Database/Indexes/MediaFolder_ByHierarchy.cs deleted file mode 100644 index 7146ba68..00000000 --- a/old/zero.Core/Database/Indexes/MediaFolder_ByHierarchy.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class MediaFolder_ByHierarchy : ZeroIndex - { - public class Result : ZeroIdEntity, IZeroDbConventions - { - public string Name { get; set; } - - public List Path { get; set; } = new List(); - } - - - public class PathResult - { - public string Id { get; set; } - - public string Name { get; set; } - } - - - protected override void Create() - { - Map = items => items.Select(item => new Result - { - Id = item.Id, - Name = item.Name, - Path = Recurse(item, x => LoadDocument(x.ParentId)) - .Where(x => x != null && x.Id != null && x.Id != item.Id) - .Reverse() - .Select(current => new PathResult() - { - Id = current.Id, - Name = current.Name - }) - .ToList() - }); - - StoreAllFields(FieldStorage.Yes); - //Index(x => x.ChannelId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/MediaFolders_WithChildren.cs b/old/zero.Core/Database/Indexes/MediaFolders_WithChildren.cs deleted file mode 100644 index 57997362..00000000 --- a/old/zero.Core/Database/Indexes/MediaFolders_WithChildren.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class MediaFolders_WithChildren : ZeroIndex - { - public class Result : ZeroIdEntity - { - public string ParentId { get; set; } - - public string Name { get; set; } - - public string[] ChildrenIds { get; set; } - } - - - protected override void Create() - { - Map = items => items.Where(x => x.ParentId != null).Select(item => new Result - { - Id = item.Id, - ParentId = item.ParentId, - Name = item.Name, - ChildrenIds = new string[] { } - }); - - Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result() - { - Id = group.Key.ParentId, - ParentId = group.Key.ParentId, - Name = String.Empty, - ChildrenIds = group.Select(x => x.Id).ToArray() - }); - - StoreAllFields(FieldStorage.Yes); - //Index(x => x.ChannelId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Media_ByChildren.cs b/old/zero.Core/Database/Indexes/Media_ByChildren.cs deleted file mode 100644 index c25d97bc..00000000 --- a/old/zero.Core/Database/Indexes/Media_ByChildren.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Media_ByChildren : ZeroMultiMapIndex - { - public class Result : ZeroIdEntity, IZeroDbConventions - { - public string ParentId { get; set; } - - public int ChildrenCount { get; set; } - - public string[] ChildrenIds { get; set; } - } - - - protected override void Create() - { - AddMap(items => items.Select(item => new Result() - { - Id = item.Id, - ParentId = item.FolderId, - ChildrenCount = 1, - ChildrenIds = new string[] { } - })); - - AddMap(items => items.Select(item => new Result() - { - Id = item.Id, - ParentId = item.ParentId, - ChildrenCount = 1, - ChildrenIds = new string[] { } - })); - - Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result() - { - Id = null, - ParentId = group.Key.ParentId, - ChildrenCount = group.Sum(x => x.ChildrenCount), - ChildrenIds = group.Select(x => x.Id).ToArray() - }); - - StoreAllFields(FieldStorage.Yes); - Index(x => x.ParentId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Media_ByParent.cs b/old/zero.Core/Database/Indexes/Media_ByParent.cs deleted file mode 100644 index fd137988..00000000 --- a/old/zero.Core/Database/Indexes/Media_ByParent.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Media_ByParent : ZeroMultiMapIndex - { - protected override void Create() - { - AddMap(items => items.Select(item => new MediaListItem - { - Id = item.Id, - ParentId = item.ParentId, - CreatedDate = item.CreatedDate, - IsFolder = true, - Name = item.Name, - Image = null, - Children = 0, - Size = 0, - IsShared = item.Blueprint != null, - AspectRatio = 0 - })); - - AddMap(items => items.Select(item => new MediaListItem - { - Id = item.Id, - ParentId = item.FolderId, - CreatedDate = item.CreatedDate, - IsFolder = false, - Name = item.Name, - Image = item.PreviewSource, - Children = 0, - Size = item.Size, - IsShared = item.Blueprint != null, - AspectRatio = item.ImageMeta != null ? (float)item.ImageMeta.Width / item.ImageMeta.Height : 0 - })); - - StoreAllFields(FieldStorage.Yes); - Index(x => x.ParentId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Pages_AsHistory.cs b/old/zero.Core/Database/Indexes/Pages_AsHistory.cs deleted file mode 100644 index e4311f84..00000000 --- a/old/zero.Core/Database/Indexes/Pages_AsHistory.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Pages_AsHistory : ZeroIndex - { - public class Result : ZeroIdEntity, IZeroDbConventions - { - public DateTimeOffset LastModified { get; set; } - } - - - protected override void Create() - { - Map = items => items - .Select(x => new Result() - { - Id = x.Id, - LastModified = x.LastModifiedDate - }); - - StoreAllFields(FieldStorage.Yes); - //Index(x => x.ChannelId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Pages_ByHierarchy.cs b/old/zero.Core/Database/Indexes/Pages_ByHierarchy.cs deleted file mode 100644 index 5481431d..00000000 --- a/old/zero.Core/Database/Indexes/Pages_ByHierarchy.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Pages_ByHierarchy : ZeroIndex - { - public class Result : ZeroIdEntity, IZeroDbConventions - { - public string Name { get; set; } - - public List Path { get; set; } = new List(); - - public string[] PathIds { get; set; } = Array.Empty(); - } - - - public class PathResult - { - public string Id { get; set; } - - public string Name { get; set; } - } - - - protected override void Create() - { - Map = items => items - .Select(item => new - { - Item = item, - Path = Recurse(item, x => LoadDocument(x.ParentId)).Where(x => x != null && x.Id != null && x.Id != item.Id).Reverse() - }) - .Select(item => new Result - { - Id = item.Item.Id, - Name = item.Item.Name, - Path = item.Path.Select(current => new PathResult() { Id = current.Id, Name = current.Name }).ToList(), - PathIds = item.Path.Select(current => current.Id).ToArray() - }); - - StoreAllFields(FieldStorage.Yes); - Index("PathIds", FieldIndexing.Exact); - //Index(x => x.ChannelId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Pages_ByType.cs b/old/zero.Core/Database/Indexes/Pages_ByType.cs deleted file mode 100644 index 3fc94dfa..00000000 --- a/old/zero.Core/Database/Indexes/Pages_ByType.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Pages_ByType : ZeroIndex - { - protected override void Create() - { - Map = items => items.Select(item => new - { - PageTypeAlias = item.PageTypeAlias - }); - - Index(x => x.PageTypeAlias, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/Pages_WithChildren.cs b/old/zero.Core/Database/Indexes/Pages_WithChildren.cs deleted file mode 100644 index 442e4138..00000000 --- a/old/zero.Core/Database/Indexes/Pages_WithChildren.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class Pages_WithChildren : ZeroIndex - { - public class Result : ZeroIdEntity, IZeroDbConventions - { - public string ParentId { get; set; } - - public string Name { get; set; } - - public string[] ChildrenIds { get; set; } - } - - - protected override void Create() - { - Map = items => items.Where(x => x.ParentId != null).Select(item => new Result - { - Id = item.Id, - ParentId = item.ParentId, - Name = item.Name, - ChildrenIds = new string[] { } - }); - - Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result() - { - Id = group.Key.ParentId, - ParentId = group.Key.ParentId, - Name = String.Empty, - ChildrenIds = group.Select(x => x.Id).ToArray() - }); - - StoreAllFields(FieldStorage.Yes); - //Index(x => x.ChannelId, FieldIndexing.Exact); - } - } -} diff --git a/old/zero.Core/Database/Indexes/RouteRedirects_ByUrl.cs b/old/zero.Core/Database/Indexes/RouteRedirects_ByUrl.cs deleted file mode 100644 index ffb5e55a..00000000 --- a/old/zero.Core/Database/Indexes/RouteRedirects_ByUrl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq; -using zero.Core.Routing; - -namespace zero.Core.Database.Indexes -{ - public class RouteRedirects_ByUrl : ZeroIndex - { - protected override void Create() - { - Map = items => items - .Select(x => new - { - IsAutomated = x.IsAutomated, - SourceUrl = x.SourceUrl, - TargetUrl = x.TargetUrl - }); - } - } -} \ No newline at end of file diff --git a/old/zero.Core/Database/Indexes/Routes_ByDependencies.cs b/old/zero.Core/Database/Indexes/Routes_ByDependencies.cs deleted file mode 100644 index 6cf7e204..00000000 --- a/old/zero.Core/Database/Indexes/Routes_ByDependencies.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Linq; -using zero.Core.Routing; - -namespace zero.Core.Database.Indexes -{ - public class Routes_ByDependencies : ZeroIndex - { - protected override void Create() - { - Map = items => items - .Select(x => new - { - Dependencies = x.Dependencies - }); - } - } -} \ No newline at end of file diff --git a/old/zero.Core/Database/Indexes/Routes_ForResolver.cs b/old/zero.Core/Database/Indexes/Routes_ForResolver.cs deleted file mode 100644 index 4a78352c..00000000 --- a/old/zero.Core/Database/Indexes/Routes_ForResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Linq; -using zero.Core.Routing; - -namespace zero.Core.Database.Indexes -{ - public class Routes_ForResolver : ZeroIndex - { - protected override void Create() - { - Map = items => items - .Select(x => new - { - Url = x.Url, - AllowSuffix = x.AllowSuffix - }); - } - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/Applications/Application.cs b/old/zero.Core/Entities/Applications/Application.cs deleted file mode 100644 index 61e565ce..00000000 --- a/old/zero.Core/Entities/Applications/Application.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - /// - /// An application is a website. zero can host multiple websites at once which share common assets - /// - [Collection("Applications")] - public class Application : ZeroEntity - { - /// - /// Raven database name for application data - /// - public string Database { get; set; } - - /// - /// Full company or product name - /// - public string FullName { get; set; } - - /// - /// Generic contact email. Can be used in various locations - /// - public string Email { get; set; } - - /// - /// Image of the application - /// - public string ImageId { get; set; } - - /// - /// Simple image of the application (can be used as favicon) - /// - public string IconId { get; set; } - - /// - /// All assigned domains for this application - /// - public Uri[] Domains { get; set; } = Array.Empty(); - - /// - /// Features which are enabled for this application. - /// Can be user-defined and affect both backoffice and frontend - /// - public List Features { get; set; } = new(); - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/Country.cs b/old/zero.Core/Entities/Country.cs deleted file mode 100644 index d80547a4..00000000 --- a/old/zero.Core/Entities/Country.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace zero.Core.Entities -{ - public class Country : ZeroEntity - { - /// - /// Preferred countries are displayed on top in lists - /// - public bool IsPreferred { get; set; } - - /// - /// Country code (ISO 3166-1) - /// - public string Code { get; set; } - } -} diff --git a/old/zero.Core/Entities/Culture.cs b/old/zero.Core/Entities/Culture.cs deleted file mode 100644 index df81cafb..00000000 --- a/old/zero.Core/Entities/Culture.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace zero.Core.Entities -{ - public class Culture - { - public string Code { get; set; } - - public string Name { get; set; } - } -} diff --git a/old/zero.Core/Entities/Language.cs b/old/zero.Core/Entities/Language.cs deleted file mode 100644 index 63aa61ce..00000000 --- a/old/zero.Core/Entities/Language.cs +++ /dev/null @@ -1,28 +0,0 @@ -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - [Collection("Languages")] - public class Language : ZeroEntity - { - /// - /// Language code (ISO 3166-1) - /// - public string Code { get; set; } - - /// - /// Whether this is the default language - /// - public bool IsDefault { get; set; } - - /// - /// Whether this language is optional and does not have to be filled out - /// - public bool IsOptional { get; set; } - - /// - /// If this language is inherited it gets all missing properties from its parent - /// - public string InheritedLanguageId { get; set; } - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/MailTemplate.cs b/old/zero.Core/Entities/MailTemplate.cs deleted file mode 100644 index 30aa4510..00000000 --- a/old/zero.Core/Entities/MailTemplate.cs +++ /dev/null @@ -1,48 +0,0 @@ -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - [Collection("MailTemplates")] - public class MailTemplate : ZeroEntity - { - /// - /// Email address of the sender (overrides email from application) - /// - public string SenderEmail { get; set; } - - /// - /// Name of the sender (overrides name from application) - /// - public string SenderName { get; set; } - - /// - /// Email address of the recipient. This is only necessary for templates which do not have a dynamic recipient (e.g. reports). - /// - public string RecipientEmail { get; set; } - - /// - /// Additional comma-separated emails to send a copy to - /// - public string Cc { get; set; } - - /// - /// Additional comma-separated emails to send a hidden copy to - /// - public string Bcc { get; set; } - - /// - /// Email subject (can contain placeholders) - /// - public string Subject { get; set; } - - /// - /// Email body (can contain placeholders) - /// - public string Body { get; set; } - - /// - /// Preheader which is displayed in the preview pane (can contain placeholders) - /// - public string Preheader { get; set; } - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/Media/Media.cs b/old/zero.Core/Entities/Media/Media.cs deleted file mode 100644 index 722c39d4..00000000 --- a/old/zero.Core/Entities/Media/Media.cs +++ /dev/null @@ -1,74 +0,0 @@ -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - /// - /// A media file (can contain an image or other media like videos and documents) - /// - [Collection("Media")] - public class Media : ZeroEntity - { - /// - /// Id/name of the phyiscal folder which is stored on disk/cloud - /// - public string FileId { get; set; } - - /// - /// Id of the media folder - /// - public string FolderId { get; set; } - - /// - /// Alternative text which is used when the image can't be loaded - /// - public string AlternativeText { get; set; } - - /// - /// Additional caption text - /// - public string Caption { get; set; } - - /// - /// Path of the media item - /// - public string Source { get; set; } - - /// - /// For images this is the source for a 100x100px thumbnail - /// - public string ThumbnailSource { get; set; } - - /// - /// For images this is the source for a [proportional]x210px thumbnail - /// - public string PreviewSource { get; set; } - - /// - /// Filesize in bytes - /// - public long Size { get; set; } - - /// - /// Meta data for images - /// - public MediaImageMeta ImageMeta { get; set; } - - /// - /// Optional focal point for an image - /// - public MediaFocalPoint FocalPoint { get; set; } - - /// - /// Type of the media - /// - public MediaType Type { get; set; } - } - - - public enum MediaSourceSize - { - Original = 0, - Preview = 1, - Thumbnail = 2 - } -} diff --git a/old/zero.Core/Entities/Media/MediaFocalPoint.cs b/old/zero.Core/Entities/Media/MediaFocalPoint.cs deleted file mode 100644 index 2142594f..00000000 --- a/old/zero.Core/Entities/Media/MediaFocalPoint.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace zero.Core.Entities -{ - /// - /// The focal point sets the point of interest in an image with x/y coordinates from 0-1 - /// - public class MediaFocalPoint - { - public decimal Left { get; set; } - - public decimal Top { get; set; } - } -} diff --git a/old/zero.Core/Entities/Media/MediaFolder.cs b/old/zero.Core/Entities/Media/MediaFolder.cs deleted file mode 100644 index e92bda9f..00000000 --- a/old/zero.Core/Entities/Media/MediaFolder.cs +++ /dev/null @@ -1,16 +0,0 @@ -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - /// - /// A media folder contains media and other folders - /// - [Collection("MediaFolders")] - public class MediaFolder : ZeroEntity - { - /// - /// Parent folder id - /// - public string ParentId { get; set; } - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/Media/MediaImageMeta.cs b/old/zero.Core/Entities/Media/MediaImageMeta.cs deleted file mode 100644 index e0558dcd..00000000 --- a/old/zero.Core/Entities/Media/MediaImageMeta.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace zero.Core.Entities -{ - /// - /// Metadata for images - /// - public class MediaImageMeta - { - /// - /// Width in pixels - /// - public int Width { get; set; } - - /// - /// Height in pixels - /// - public int Height { get; set; } - - /// - /// Resolution factor - /// - public double DPI { get; set; } - - /// - /// Date the image was taken - /// - public DateTimeOffset? CreatedDate { get; set; } - - /// - /// Original color space of the image - /// - public string ColorSpace { get; set; } - - /// - /// Whether this image contains transparent pixels - /// - public bool HasTransparency { get; set; } - - /// - /// How many frames contains this image (for animation) - /// - public int Frames { get; set; } = 1; - } -} diff --git a/old/zero.Core/Entities/Media/MediaListItem.cs b/old/zero.Core/Entities/Media/MediaListItem.cs deleted file mode 100644 index c812073d..00000000 --- a/old/zero.Core/Entities/Media/MediaListItem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace zero.Core.Entities -{ - public class MediaListItem : ZeroIdEntity - { - public string ParentId { get; set; } - - public string Name { get; set; } - - public DateTimeOffset CreatedDate { get; set; } - - public bool IsFolder { get; set; } - - public string Image { get; set; } - - public long Size { get; set; } - - public int Children { get; set; } - - public bool HasTransparency { get; set; } - - public float AspectRatio { get; set; } - - public bool IsShared { get; set; } - } -} diff --git a/old/zero.Core/Entities/Media/MediaListQuery.cs b/old/zero.Core/Entities/Media/MediaListQuery.cs deleted file mode 100644 index c36f07f5..00000000 --- a/old/zero.Core/Entities/Media/MediaListQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace zero.Core.Entities -{ - public class MediaListQuery : ListQuery - { - public string FolderId { get; set; } - } - - - public class MediaListItemQuery : ListQuery - { - public string FolderId { get; set; } - - public bool SearchIsGlobal { get; set; } - } -} diff --git a/old/zero.Core/Entities/Media/MediaType.cs b/old/zero.Core/Entities/Media/MediaType.cs deleted file mode 100644 index b33ff27b..00000000 --- a/old/zero.Core/Entities/Media/MediaType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace zero.Core.Entities -{ - public enum MediaType - { - File = 0, - Image = 1 - } -} diff --git a/old/zero.Core/Entities/Refs/MediaRef.cs b/old/zero.Core/Entities/Refs/MediaRef.cs deleted file mode 100644 index b610e7fb..00000000 --- a/old/zero.Core/Entities/Refs/MediaRef.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace zero.Core.Entities -{ - public class MediaRef : Ref - { - public bool IsCore { get; set; } - - public MediaRef() : base() { } - public MediaRef(string id, bool isCore) : base(id) - { - IsCore = isCore; - } - - public static implicit operator string(MediaRef reference) => reference.Id; - - public static implicit operator MediaRef(string id) => new MediaRef(id, false); - } -} diff --git a/old/zero.Core/Entities/Refs/Ref.cs b/old/zero.Core/Entities/Refs/Ref.cs deleted file mode 100644 index 2934ad5f..00000000 --- a/old/zero.Core/Entities/Refs/Ref.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace zero.Core.Entities -{ - public class Ref : Ref where T : ZeroIdEntity - { - public Ref() : base() { } - public Ref(string id) : base(id) { } - - public static implicit operator Ref(string id) => new Ref(id); - - public static implicit operator string(Ref reference) => reference.Id; - } - - - public class Ref - { - public Ref() { } - - public Ref(string id) - { - Id = id; - } - - public string Id { get; set; } - - - public override string ToString() - { - return Id; - } - - - public static implicit operator Ref(string id) => new Ref(id); - - public static implicit operator string(Ref reference) => reference.Id; - } -} diff --git a/old/zero.Core/Entities/Refs/ValueRef.cs b/old/zero.Core/Entities/Refs/ValueRef.cs deleted file mode 100644 index dec86607..00000000 --- a/old/zero.Core/Entities/Refs/ValueRef.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace zero.Core.Entities -{ - public class ValueRef : ValueRef where T : ZeroIdEntity - { - public ValueRef() : base() { } - public ValueRef(string id, string value) : base(id, value) { } - - public static implicit operator string(ValueRef reference) => reference.Id; - } - - - public class ValueRef : Ref where TEntity : ZeroIdEntity - { - public ValueRef() : base() { } - public ValueRef(string id, TValue value) : base(id) - { - Value = value; - } - - public TValue Value { get; set; } - - public static implicit operator string(ValueRef reference) => reference.Id; - } - - //public class ValueRef : Ref - //{ - // public ValueRef() : base() { } - - // public ValueRef(string id, string value) : base(id) - // { - // Value = value; - // } - - // public string Value { get; set; } - - - // public override string ToString() - // { - // return (Id, Value).ToString(); - // } - //} -} diff --git a/old/zero.Core/Entities/Setup/SetupModel.cs b/old/zero.Core/Entities/Setup/SetupModel.cs deleted file mode 100644 index a4b7d2a9..00000000 --- a/old/zero.Core/Entities/Setup/SetupModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace zero.Core.Entities.Setup -{ - public sealed class SetupModel - { - public string AppName { get; set; } - - public SetupUserModel User { get; set; } - - public SetupDatabaseModel Database { get; set; } - - public string ContentRootPath { get; set; } - } - - - public sealed class SetupUserModel - { - public string Name { get; set; } - - public string Email { get; set; } - - public string Password { get; set; } - } - - public sealed class SetupDatabaseModel - { - public string Url { get; set; } - - public string Name { get; set; } - } -} diff --git a/old/zero.Core/Entities/Translation.cs b/old/zero.Core/Entities/Translation.cs deleted file mode 100644 index 70421779..00000000 --- a/old/zero.Core/Entities/Translation.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace zero.Core.Entities -{ - public class Translation : ZeroEntity - { - public Translation() - { - IsActive = true; - } - - /// - /// Value of the translation - /// - public string Value { get; set; } - - /// - /// Display + input type - /// - public TranslationDisplay Display { get; set; } - } - - - public enum TranslationDisplay - { - Text = 0, - HTML = 1 - } -} \ No newline at end of file diff --git a/old/zero.Core/Entities/Video.cs b/old/zero.Core/Entities/Video.cs deleted file mode 100644 index edbc9409..00000000 --- a/old/zero.Core/Entities/Video.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace zero.Core.Entities -{ - public class Video - { - public VideoProvider Provider { get; set; } - - public string VideoId { get; set; } - - public string VideoUrl { get; set; } - - public string VideoPreviewImageUrl { get; set; } - - public string Title { get; set; } - - public string PreviewImageId { get; set; } - } - - - public enum VideoProvider - { - Html, - Youtube, - Vimeo - } -} diff --git a/old/zero.Core/Extensions/BackofficeUserExtensions.cs b/old/zero.Core/Extensions/BackofficeUserExtensions.cs deleted file mode 100644 index ce3c4b34..00000000 --- a/old/zero.Core/Extensions/BackofficeUserExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; -using zero.Core.Identity; - -namespace zero.Core.Extensions -{ - public static class BackofficeUserExtensions - { - public static string[] GetAllowedAppIds(this BackofficeUser user) - { - if (user == null) - { - return new string[0] { }; - } - - IEnumerable permissions = user.Claims - .Where(claim => claim.Type == Constants.Auth.Claims.Permission && claim.Value.StartsWith(Permissions.Applications)) - .Select(x => Permission.FromClaim(x.ToClaim(), Permissions.Applications)); - - return permissions.Where(x => x.IsTrue).Select(x => x.NormalizedKey).ToArray(); - } - } -} diff --git a/old/zero.Core/Extensions/CollectionExtensions.cs b/old/zero.Core/Extensions/CollectionExtensions.cs deleted file mode 100644 index eddba42b..00000000 --- a/old/zero.Core/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using zero.Core.Collections; - -namespace zero.Core.Extensions -{ - public static class CollectionExtensions - { - /// - /// Shared scope for the current collection instance - /// - public static CollectionScope SharedScope(this T collection) where T : ICollectionBase - { - return new CollectionScope(collection, "shared"); - } - } -} diff --git a/old/zero.Core/Extensions/ColorExtensions.cs b/old/zero.Core/Extensions/ColorExtensions.cs deleted file mode 100644 index 056244da..00000000 --- a/old/zero.Core/Extensions/ColorExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Drawing; -using System.Linq; - -namespace zero.Core.Extensions -{ - public static class ColorExtensions - { - /// - /// Get color object from a hex string - /// - public static Color FromHex(this string color) - { - if (color.StartsWith("#")) - { - color = color[1..]; - } - - if (color.Length != 6) - { - throw new Exception("Hex color has to be in format #RRGGBB"); - } - - return Color.FromArgb( - int.Parse(color.Substring(0, 2), System.Globalization.NumberStyles.HexNumber), - int.Parse(color.Substring(2, 2), System.Globalization.NumberStyles.HexNumber), - int.Parse(color.Substring(4, 2), System.Globalization.NumberStyles.HexNumber) - ); - } - - - /// - /// Get contrast ratio between two color values - /// - /// - public static double GetContrastRatio(this Color color1, Color color2) - { - double lum1 = color1.GetLuminance(); - double lum2 = color2.GetLuminance(); - double brightest = Math.Max(lum1, lum2); - double darkest = Math.Min(lum1, lum2); - return (brightest + 0.05) / (darkest + 0.05); - } - - - /// - /// Get luminance value of a color based on WCAG-conform JS implementation - /// - /// - public static double GetLuminance(this Color color) - { - double[] a = new double[3] { color.R, color.G, color.B }.Select(v => - { - v /= 255; - return v <= 0.03928 ? v / 12.92 : Math.Pow((v + 0.055) / 1.055, 2.4); - }).ToArray(); - - return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; - } - } -} diff --git a/old/zero.Core/Extensions/EnumerableExtensions.cs b/old/zero.Core/Extensions/EnumerableExtensions.cs deleted file mode 100644 index 9f384396..00000000 --- a/old/zero.Core/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Extensions -{ - public static class EnumerableExtensions - { - /// - /// - /// - public static ListResult ToQueriedList(this IEnumerable items, ListQuery query) where T : ZeroEntity - { - //queryable = queryable.Statistics(out QueryStatistics stats); - - if (query != null) - { - if (!query.IncludeInactive) - { - items = items.Where(x => x.IsActive); - } - - if (!query.Search.IsNullOrEmpty()) - { - items = items.Where(x => x.Name.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase)); - } - //if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) - //{ - // items = items.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); - //} - //if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) - //{ - // foreach (var selector in query.SearchSelectors) - // { - // items = items.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); - // } - //} - - //if (!query.OrderBy.IsNullOrEmpty()) - //{ - // items = items.OrderBy(query.OrderBy, query.OrderIsDescending); - //} - - if (query.PageSize > 0) - { - items = items.Paging(query.Page, query.PageSize); - } - } - - List result = items.ToList(); - - return new ListResult(result, result.Count, query.Page, query.PageSize); - } - - - - public static IEnumerable Paging(this IEnumerable source, int pageNumber, int pageSize) - { - pageNumber = pageNumber.Limit(1, 10_000_000); - pageSize = pageSize.Limit(1, 1_000); - - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); - } - - - public static bool TryGet(this IEnumerable source, Func predicate, out T model) - { - model = source.FirstOrDefault(predicate); - return model != null; - } - - - public static bool TryAdd(this IList source, T model) where T : class - { - if (model == default) - { - return false; - } - - source.Add(model); - return true; - } - } -} diff --git a/old/zero.Core/Extensions/HttpContextExtensions.cs b/old/zero.Core/Extensions/HttpContextExtensions.cs deleted file mode 100644 index 2edc77de..00000000 --- a/old/zero.Core/Extensions/HttpContextExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace zero.Core.Extensions -{ - public static class HttpContextExtensions - { - /// - /// Whether the current request is a backoffice request - /// - public static bool IsBackofficeRequest(this HttpContext context, string backofficePath) - { - string path = backofficePath.EnsureStartsWith('/').TrimEnd('/'); - return context.Request.Path.ToString().StartsWith(path); - } - - - /// - /// Whether the current request is an AJAX request - /// - public static bool IsAjaxRequest(this HttpContext context) - { - if (context?.Request?.Headers == null) - { - return false; - } - - return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest"; - } - - - /// - /// Whether the current request only accepts application/json - /// - public static bool IsJsonRequest(this HttpContext context) - { - if (context?.Request?.Headers == null) - { - return false; - } - - return context.Request.Headers["Accept"] == "application/json"; - } - - - /// - /// Get IP Address of the client - /// - public static string GetClientIpAddress(this HttpContext context) - { - return context.Connection.RemoteIpAddress?.ToString(); - //string ipAddress = context.GetServerVariable("HTTP_X_FORWARDED_FOR"); - //return !ipAddress.IsNullOrEmpty() ? ipAddress.Split(',')[0] : context.GetServerVariable("REMOTE_ADDR"); - } - } -} diff --git a/old/zero.Core/Extensions/MailExtensions.cs b/old/zero.Core/Extensions/MailExtensions.cs deleted file mode 100644 index 6cb97377..00000000 --- a/old/zero.Core/Extensions/MailExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Mail; -using zero.Core.Entities; - -namespace zero.Core.Extensions -{ - public static class MailExtensions - { - //public static string Add(this AttachmentCollection attachments, byte[] content, ) - //{ - // if (input < min) - // { - // return min; - // } - // if (input > max) - // { - // return max; - // } - // return input; - //} - } -} diff --git a/old/zero.Core/Extensions/NumberExtensions.cs b/old/zero.Core/Extensions/NumberExtensions.cs deleted file mode 100644 index 553de5e6..00000000 --- a/old/zero.Core/Extensions/NumberExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Extensions -{ - public static class NumberExtensions - { - static Dictionary FileSizeUnits = new() - { - { FileSizeNotation.SI, new string[] { "B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" } }, - { FileSizeNotation.IEC, new string[] { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" } }, - { FileSizeNotation.JEDEC, new string[] { "B", "KB", "MB", "GB" } } - }; - - public static int Limit(this int input, int min, int max) - { - if (input < min) - { - return min; - } - if (input > max) - { - return max; - } - return input; - } - - - public static string GetFileSize(this long sizeInBytes, FileSizeNotation notation = FileSizeNotation.JEDEC) - { - if (!FileSizeUnits.ContainsKey(notation)) - { - throw new NotImplementedException($"The notation {notation} has no implementation for generating human-readable file sizes"); - } - - int power = notation == FileSizeNotation.SI ? 1000 : 1024; - - string[] units = FileSizeUnits[notation]; - - int order = 0; - - while (sizeInBytes >= power && order + 1 < units.Length) - { - order++; - sizeInBytes = sizeInBytes / power; - } - - return String.Format("{0:0.##} {1}", sizeInBytes, units[order]); - } - } -} diff --git a/old/zero.Core/Extensions/ObjectExtensions.cs b/old/zero.Core/Extensions/ObjectExtensions.cs deleted file mode 100644 index 6207080f..00000000 --- a/old/zero.Core/Extensions/ObjectExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using zero.Core.Attributes; -using zero.Core.Utils; - -namespace zero.Core.Extensions -{ - public static class ObjectExtensions - { - public static bool Is(this Type type) - { - return type.IsAssignableFrom(typeof(T)); - } - - - public static bool Is(this object obj) - { - return obj.GetType().IsAssignableFrom(typeof(T)); - } - - - public static T Clone(this T obj) - { - Type type = obj.GetType(); - return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), type, new RefJsonConverter()); - } - - public static T CloneLax(this object obj) - { - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), new RefJsonConverter()); - } - - public static T AutoSetIds(this T obj) - { - // find all Raven Ids - List> ravenIds = ObjectTraverser.FindAttribute(obj); - - // set unset Raven Ids - foreach (ObjectTraverser.Result item in ravenIds) - { - string id = item.Property.GetValue(item.Parent, null) as string; - if (String.IsNullOrWhiteSpace(id)) - { - item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create()); - } - } - - return obj; - } - } -} diff --git a/old/zero.Core/Extensions/QueryableExtensions.cs b/old/zero.Core/Extensions/QueryableExtensions.cs deleted file mode 100644 index 547cfc01..00000000 --- a/old/zero.Core/Extensions/QueryableExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using Raven.Client.Documents.Queries; -using Raven.Client.Documents.Session; -using System; -using System.Linq; -using System.Linq.Expressions; - -namespace zero.Core.Extensions -{ - public static class QueryableExtensions - { - public static IOrderedQueryable OrderBy(this IQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) - { - if (!String.IsNullOrEmpty(path)) - { - char[] a = path.ToCharArray(); - a[0] = char.ToUpper(a[0]); - path = new string(a); - } - - if (isDescending) - { - return source.OrderByDescending(path, type); - } - return source.OrderBy(path, type); - } - - - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) - { - if (!String.IsNullOrEmpty(path)) - { - char[] a = path.ToCharArray(); - a[0] = char.ToUpper(a[0]); - path = new string(a); - } - - if (isDescending) - { - return source.ThenByDescending(path, type); - } - return source.ThenBy(path, type); - } - - - public static IQueryable Paging(this IQueryable source, int pageNumber, int pageSize) - { - pageNumber = pageNumber.Limit(1, 10_000_000); - pageSize = pageSize.Limit(1, 1_000); - - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); - } - - - public static IRavenQueryable WhereIf(this IRavenQueryable source, Expression> predicate, bool condition, Expression> elsePredicate = null) - { - if (!condition) - { - if (elsePredicate != null) - { - return source.Where(elsePredicate); - } - return source; - } - - return source.Where(predicate); - } - - - public static IQueryable SearchIf(this IQueryable source, Expression> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or) - { - if (String.IsNullOrWhiteSpace(searchTerms)) - { - return source; - } - - searchTerms = searchTerms.Trim(); - - string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => - { - if (suffix != null) - { - x += suffix; - } - if (prefix != null) - { - x = prefix + x; - } - return x; - }).ToArray(); - - if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"')) - { - searchParts = new[] { searchTerms }; - } - - return source.Search(fieldSelector, searchParts, @operator: @operator); - } - } -} diff --git a/old/zero.Core/Extensions/ServiceCollectionExtensions.cs b/old/zero.Core/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 6c93cc89..00000000 --- a/old/zero.Core/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using zero.Core.Assemblies; - -namespace zero.Core.Extensions -{ - public static class ServiceCollectionExtensions - { - /// - /// Adds all found implementations based on the service type and assembly discovery rules - /// - public static void AddAll(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false) => services.AddAll(typeof(TService), lifetime, asSelf); - - - /// - /// Adds all found implementations based on the service type and assembly discovery rules - /// - public static void AddAll(this IServiceCollection services, Type serviceType, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false) - { - if (AssemblyDiscovery.Current == null) - { - throw new Exception("services.AddAll() can only be run after mvcBuilder.AddZero()"); - } - - // add implementations with generic service types - if (serviceType.GetTypeInfo().IsGenericTypeDefinition) - { - IEnumerable<(Type, TypeInfo)> matches = AssemblyDiscovery.Current.GetAllClassTypes().SelectMany(type => - { - var interfaces = type.GetInterfaces().Select(x => x.GetTypeInfo()); - IEnumerable genericTypes = interfaces.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == serviceType); - if (genericTypes.Any()) - { - var genericTypeDefinitions = genericTypes.First(); - } - return genericTypes.Select(x => (x, type)); - }); - - foreach ((Type, Type) match in matches) - { - services.Add(new ServiceDescriptor(asSelf ? match.Item2 : match.Item1, match.Item2, lifetime)); - } - } - // add implementations with specific service types - else - { - foreach (Type type in AssemblyDiscovery.Current.GetTypes(serviceType)) - { - services.Add(new ServiceDescriptor(asSelf ? type : serviceType, type, lifetime)); - } - } - } - - - /// - /// Adds or overrides an implementation - /// - public static void Replace(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient) - where TService : class - where TImplementation : class, TService - { - services.RemoveAll(); - services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime)); - } - - - /// - /// Adds or overrides an implementation - /// - public static void Replace(this IServiceCollection services, Func implementationFactory, ServiceLifetime lifetime = ServiceLifetime.Transient) - where TService : class - where TImplementation : class, TService - { - services.RemoveAll(); - services.Add(new ServiceDescriptor(typeof(TService), implementationFactory, lifetime)); - } - } -} diff --git a/old/zero.Core/Extensions/ValidatorExtensions.cs b/old/zero.Core/Extensions/ValidatorExtensions.cs deleted file mode 100644 index 41608c50..00000000 --- a/old/zero.Core/Extensions/ValidatorExtensions.cs +++ /dev/null @@ -1,246 +0,0 @@ -using FluentValidation; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using zero.Core.Api; -using zero.Core.Database; -using zero.Core.Entities; - -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.forms.hex_format"); - } - - - /// - /// Validate an email - /// - public static IRuleBuilderOptions Url(this IRuleBuilder ruleBuilder) - { - return ruleBuilder.Must((root, value, context) => - { - return value.IsNullOrWhiteSpace() || Uri.IsWellFormedUriString(value, UriKind.Absolute); - }).WithMessage("@errors.forms.url_format"); - } - - - /// - /// 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.forms.email_invalid"); - } - - - /// - /// 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.forms.emails_invalid"); - } - - - /// - /// Check if this value is unique within a collection - /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - bool any = await store.Session().Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), value) - .AnyAsync(cancellation); - - return !any; - }).WithMessage("@errors.forms.not_unique"); - } - - - /// - /// Check if this value is at least set once to the expected value within a collection - /// - public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - return await store.Session().Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) - .AnyAsync(cancellation); - }).WithMessage("@errors.forms.not_unique_alone"); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity - { - return ruleBuilder.Exists(store); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity where TReference : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => - { - if (id.IsNullOrWhiteSpace()) - { - return true; - } - - TReference model = await store.Session().LoadAsync(id); - return model != null; - }).WithMessage("@errors.forms.reference_notfound"); - } - - - /// - /// Validates a culture identifier - /// - public static IRuleBuilderOptions Culture(this IRuleBuilder ruleBuilder) - { - return ruleBuilder.Must((root, value, context) => - { - if (value.IsNullOrWhiteSpace()) - { - return true; - } - - try - { - CultureInfo info = CultureInfo.GetCultureInfo(value); - return info != null && !info.EnglishName.Equals(value, StringComparison.InvariantCultureIgnoreCase); - } - catch - { - return false; - } - }).WithMessage("@errors.forms.culture"); - } - - - - - /// - /// Check if this value is unique within a collection - /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - bool any = await session.Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), value) - .AnyAsync(cancellation); - - return !any; - }).WithMessage("@errors.forms.not_unique"); - } - - - /// - /// Check if this value is at least set once to the expected value within a collection - /// - public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session, TProperty expectedValue) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - return await session.Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) - .AnyAsync(cancellation); - }).WithMessage("@errors.forms.not_unique_alone"); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity - { - return ruleBuilder.Exists(session); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity where TReference : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => - { - if (id.IsNullOrWhiteSpace()) - { - return true; - } - - TReference model = await session.LoadAsync(id); - return model != null; - }).WithMessage("@errors.forms.reference_notfound"); - } - } -} diff --git a/old/zero.Core/Handlers/IApplicationResolverHandler.cs b/old/zero.Core/Handlers/IApplicationResolverHandler.cs deleted file mode 100644 index 200328d1..00000000 --- a/old/zero.Core/Handlers/IApplicationResolverHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Handlers -{ - public interface IApplicationResolverHandler : IHandler - { - Application Resolve(HttpRequest request, IEnumerable applications); - } -} diff --git a/old/zero.Core/Handlers/IHandler.cs b/old/zero.Core/Handlers/IHandler.cs deleted file mode 100644 index c0c06e5a..00000000 --- a/old/zero.Core/Handlers/IHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace zero.Core.Handlers -{ - public interface IHandler - { - } -} diff --git a/old/zero.Core/Handlers/IHandlerHolder.cs b/old/zero.Core/Handlers/IHandlerHolder.cs deleted file mode 100644 index f9ebdf0d..00000000 --- a/old/zero.Core/Handlers/IHandlerHolder.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; - -namespace zero.Core.Handlers -{ - public class HandlerHolder : IHandlerHolder - { - IServiceProvider Services { get; } - - public HandlerHolder(IServiceProvider services) - { - Services = services; - } - - - public T Get() where T : IHandler - { - return Services.GetService(); - } - } - - - public interface IHandlerHolder - { - T Get() where T : IHandler; - } -} diff --git a/old/zero.Core/Handlers/IModuleTypeHandler.cs b/old/zero.Core/Handlers/IModuleTypeHandler.cs deleted file mode 100644 index 9c995b46..00000000 --- a/old/zero.Core/Handlers/IModuleTypeHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using zero.Core.Entities; - -namespace zero.Core.Handlers -{ - public interface IModuleTypeHandler : IHandler - { - IEnumerable GetAllowedModuleTypes(Application application, IEnumerable registeredTypes, Page page = default, string[] tags = default); - } -} diff --git a/old/zero.Core/Handlers/IPageTypeHandler.cs b/old/zero.Core/Handlers/IPageTypeHandler.cs deleted file mode 100644 index 07f0bce5..00000000 --- a/old/zero.Core/Handlers/IPageTypeHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Handlers -{ - public interface IPageTypeHandler : IHandler - { - Task> GetAllowedPageTypes(Application application, IEnumerable registeredTypes, IEnumerable parents); - } -} diff --git a/old/zero.Core/Integrations/Integration.cs b/old/zero.Core/Integrations/Integration.cs deleted file mode 100644 index df9b7e23..00000000 --- a/old/zero.Core/Integrations/Integration.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using zero.Core.Attributes; -using zero.Core.Entities; - -namespace zero.Core.Integrations -{ - /// - /// An integration is an application part which has a public configuration per app. - /// It's up to the user to provide functionality. - /// - [Collection("Integrations")] - public class Integration : ZeroEntity - { - /// - public string TypeAlias { get; set; } - } -} \ No newline at end of file diff --git a/old/zero.Core/Integrations/IntegrationService.cs b/old/zero.Core/Integrations/IntegrationService.cs deleted file mode 100644 index a6c00f78..00000000 --- a/old/zero.Core/Integrations/IntegrationService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Logging; -using zero.Core.Collections; - -namespace zero.Core.Integrations -{ - public class IntegrationService : IntegrationsCollection, IIntegrationService - { - public IntegrationService(ICollectionContext context, ILogger logger) : base(context, logger) - { - Options = new(false); - } - } - - - public interface IIntegrationService : IIntegrationsCollection { } -} diff --git a/old/zero.Core/Integrations/IntegrationType.cs b/old/zero.Core/Integrations/IntegrationType.cs deleted file mode 100644 index b4212925..00000000 --- a/old/zero.Core/Integrations/IntegrationType.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using zero.Core.Options; - -namespace zero.Core.Integrations -{ - /// - /// An integration is an application part which has a public configuration per app. - /// It's up to the user to provide functionality. - /// - public class IntegrationType : IntegrationType where T : Integration, new() - { - public IntegrationType() : base(typeof(T)) { } - } - - /// - /// An integration is an application part which has a public configuration per app. - /// It's up to the user to provide functionality. - /// - public class IntegrationType : OptionsType - { - /// - /// Group integrations by tags - /// - public List Tags { get; set; } = new(); - - /// - /// Optional description - /// - public string Description { get; set; } - - /// - /// Image of the integration - /// - public string ImagePath { get; set; } - - - public IntegrationType(Type type) - { - ContentType = type; - } - - public static IntegrationType Convert(IntegrationType model) where T : Integration, new() - { - return new(model.ContentType) - { - Alias = model.Alias, - Name = model.Name, - Description = model.Description, - ImagePath = model.ImagePath, - Tags = model.Tags, - Validator = model.Validator - }; - } - } -} diff --git a/old/zero.Core/Integrations/IntegrationTypeWithStatus.cs b/old/zero.Core/Integrations/IntegrationTypeWithStatus.cs deleted file mode 100644 index ebb6cc70..00000000 --- a/old/zero.Core/Integrations/IntegrationTypeWithStatus.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace zero.Core.Integrations -{ - public class IntegrationTypeWithStatus - { - public IntegrationType Type { get; set; } - - public bool IsActive { get; set; } - - public bool IsConfigured { get; set; } - - public string Id { get; set; } - } -} diff --git a/old/zero.Core/Mails/FileMailDispatcher.cs b/old/zero.Core/Mails/FileMailDispatcher.cs deleted file mode 100644 index f1a78ab1..00000000 --- a/old/zero.Core/Mails/FileMailDispatcher.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Mail; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Api; - -namespace zero.Core.Mails -{ - /// - /// Default implementation of an IMailSender which sends the mail a flat file - /// and therefore not using the SMTP channel. - /// Implementing real mail sending is up to the consuming application. - /// - public class FileMailDispatcher : IMailDispatcher - { - protected Queue Queue { get; private set; } = new Queue(); - - protected IPaths PathResolver { get; private set; } - - string MailDirectory; - - - public FileMailDispatcher(IPaths pathResolver) - { - PathResolver = pathResolver; - MailDirectory = "mails"; - } - - - /// - public void Enqueue(Mail message) - { - Queue.Enqueue(message); - } - - - /// - public async Task Send(CancellationToken token = default) - { - if (Queue.Count < 1) - { - return; - } - - string folder = PathResolver.Map(MailDirectory); - PathResolver.Create(folder); - - while (Queue.Count > 0) - { - Mail message = Queue.Dequeue(); - string content = JsonConvert.SerializeObject(message, new JsonSerializerSettings() - { - Formatting = Formatting.Indented, - TypeNameHandling = TypeNameHandling.None, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); - - string filename = Safenames.File(DateTimeOffset.Now.ToString("yyyy-MM-dd-HH-mm-ss") + "_" + message.To[0].Address + ".txt"); - string path = PathResolver.Map(folder, filename); - - await File.WriteAllTextAsync(path, content, token); - } - } - - - /// - public void Dispose() { } - - - /// - /// Creats the file content from a mail message - /// - string CreateContent(Mail message) - { - StringBuilder text = new(); - text.AppendLine("To: " + message.To); - text.AppendLine("CC: " + message.CC); - text.AppendLine("Bcc: " + message.Bcc); - text.AppendLine("From: " + message.From); - text.AppendLine("Subject: " + message.Subject); - text.Append("Date: " + DateTimeOffset.UtcNow.ToString()); - - text.AppendLine(); - text.AppendLine("=============== BODY ==============="); - text.AppendLine(); - - text.AppendLine(message.Body); - - return text.ToString(); - } - } -} diff --git a/old/zero.Core/Mails/IMailDispatcher.cs b/old/zero.Core/Mails/IMailDispatcher.cs deleted file mode 100644 index d9c7e6f1..00000000 --- a/old/zero.Core/Mails/IMailDispatcher.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Net.Mail; -using System.Threading; -using System.Threading.Tasks; - -namespace zero.Core.Mails -{ - public interface IMailDispatcher : IDisposable - { - /// - /// Adds a new mail message to the outgoing queue - /// - void Enqueue(Mail message); - - /// - /// Sends all mails which have been added to the queue previously - /// - Task Send(CancellationToken token = default); - - /// - /// Whether a certain sender signature is supported by this dispatcher - /// - Task IsSenderSupported(string email) => Task.FromResult(true); - } -} diff --git a/old/zero.Core/Mails/LoggerMailDispatcher.cs b/old/zero.Core/Mails/LoggerMailDispatcher.cs deleted file mode 100644 index 227ba3ec..00000000 --- a/old/zero.Core/Mails/LoggerMailDispatcher.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Collections.Generic; -using System.Net.Mail; -using System.Threading; -using System.Threading.Tasks; - -namespace zero.Core.Mails -{ - /// - /// Default implementation of an IMailSender which sends the mail to the attached logger - /// and therefore not using the SMTP channel. - /// Implementing real mail sending is up to the consuming application. - /// - public class LoggerMailDispatcher : IMailDispatcher - { - protected Queue Queue { get; private set; } = new Queue(); - - protected ILogger Logger { get; set; } - - - public LoggerMailDispatcher(ILogger logger) - { - Logger = logger; - } - - - /// - public void Enqueue(Mail message) - { - Queue.Enqueue(message); - } - - - /// - public Task Send(CancellationToken token = default) - { - while (Queue.Count > 0) - { - Mail message = Queue.Dequeue(); - Logger.LogInformation("Mail to {to}. Subject: {subject}", message.To[0].Address, message.Subject); - } - - return Task.CompletedTask; - } - - - /// - public void Dispose() { } - } -} diff --git a/old/zero.Core/Mails/Mail.cs b/old/zero.Core/Mails/Mail.cs deleted file mode 100644 index fca4ad91..00000000 --- a/old/zero.Core/Mails/Mail.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Net.Mail; -using zero.Core.Entities; - -namespace zero.Core.Mails -{ - public class Mail : Mail where T : class - { - public T Model { get; set; } - } - - public class Mail : MailMessage - { - public bool IsDeactivated { get; set; } - - public string ViewPath { get; set; } - - public bool HasView { get; set; } = true; - - public bool IsRendered { get; set; } - - public string Preheader { get; set; } - - public MailTemplate Template { get; set; } - - public MailPlaceholders Placeholders { get; set; } = new(); - - public MailMetadata Metadata { get; set; } = new(); - } -} diff --git a/old/zero.Core/Mails/MailMetadata.cs b/old/zero.Core/Mails/MailMetadata.cs deleted file mode 100644 index 5474334c..00000000 --- a/old/zero.Core/Mails/MailMetadata.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace zero.Core.Mails -{ - public class MailMetadata : Dictionary - { - - } -} diff --git a/old/zero.Core/Mails/MailPlaceholders.cs b/old/zero.Core/Mails/MailPlaceholders.cs deleted file mode 100644 index 16378b04..00000000 --- a/old/zero.Core/Mails/MailPlaceholders.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace zero.Core.Mails -{ - public class MailPlaceholders : Dictionary - { - - } -} diff --git a/old/zero.Core/Mails/MailProvider.cs b/old/zero.Core/Mails/MailProvider.cs deleted file mode 100644 index 0a97e54a..00000000 --- a/old/zero.Core/Mails/MailProvider.cs +++ /dev/null @@ -1,209 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Net.Mail; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Renderer; - -namespace zero.Core.Mails -{ - public class MailProvider : IMailProvider - { - protected IMailTemplatesCollection Collection { get; set; } - - protected ILogger Logger { get; set; } - - protected IZeroContext Zero { get; set; } - - protected IMailDispatcher MailSender { get; set; } - - protected IRazorRenderer Renderer { get; set; } - - protected Regex PlaceholderRegex { get; set; } - - private Encoding encoding = Encoding.UTF8; - - - public MailProvider(IZeroContext zero, IMailTemplatesCollection collection, ILogger logger, IMailDispatcher mailSender, IRazorRenderer renderer) - { - Zero = zero; - Collection = collection; - Logger = logger; - MailSender = mailSender; - Renderer = renderer; - PlaceholderRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase); - } - - - /// - public virtual async Task Create(string mailTemplateKey, Action onCreate = null) where T : Mail, new() - { - MailTemplate template = await GetMailTemplate(mailTemplateKey); - - if (template == null) - { - Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey); - return null; - } - - onCreate?.Invoke(template); - - return Merge(new T(), template); - } - - - /// - public virtual async Task Create(string mailTemplateKey, Action onCreate = null) - { - MailTemplate template = await GetMailTemplate(mailTemplateKey); - - if (template == null) - { - Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey); - return null; - } - - onCreate?.Invoke(template); - - return Merge(new Mail(), template); - } - - - /// - public virtual async Task Send(Mail message, CancellationToken token = default) - { - await Send(message, MailSender, token); - } - - - /// - public virtual async Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default) - { - if (message.IsDeactivated) - { - return; - } - - try - { - await Render(message); - dispatcher.Enqueue(message); - await dispatcher.Send(token); - Logger.LogInformation("Dispatched email (template: {template}) to {recipient}", message.Template?.Alias ?? "none", message.To); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to send mail message with template @{template}", message.Template.Key); - } - } - - - - protected virtual async Task GetMailTemplate(string key) - { - return await Collection.GetByKey(key); - } - - - /// - public virtual async Task Render(Mail message) - { - message.Subject = TokenReplacement.Apply(message.Subject, message.Placeholders); - message.Body = TokenReplacement.Apply(message.Body, message.Placeholders); - message.Preheader = TokenReplacement.Apply(message.Preheader, message.Placeholders); - - if (!message.HasView) - { - message.IsRendered = true; - return message.Body; - } - - string viewPath = message.ViewPath; - - if (viewPath.IsNullOrEmpty()) - { - viewPath = $"~/Views/Mails/{message.Template.Key.Replace('.', '/')}.cshtml"; - } - - message.Body = await Renderer.ViewAsync(viewPath, message); - message.IsBodyHtml = true; - message.IsRendered = true; - return message.Body; - } - - - protected virtual T Merge(T mail, MailTemplate template) where T : Mail - { - mail.Template = template; - mail.IsDeactivated = !mail.Template.IsActive; - - // get sender from template or fall back to application - mail.From = new MailAddress(template.SenderEmail.Or(Zero.Application.Email), template.SenderName.Or(Zero.Application.FullName), encoding); - mail.Sender = mail.From; - mail.ReplyToList.Add(mail.From); - - // cc + bcc (multiple addresses are separated with commas - if (!template.Cc.IsNullOrWhiteSpace()) - { - mail.CC.Add(template.Cc); - } - if (!template.Bcc.IsNullOrWhiteSpace()) - { - mail.Bcc.Add(template.Bcc); - } - - // recipient - if (!template.RecipientEmail.IsNullOrWhiteSpace()) - { - mail.To.Add(template.RecipientEmail); - } - - // subject - mail.Subject = template.Subject; - mail.SubjectEncoding = encoding; - - // body - mail.Body = mail.Template.Body; - mail.IsBodyHtml = true; - mail.BodyEncoding = encoding; - mail.Preheader = mail.Template.Preheader; - - return mail; - } - } - - - public interface IMailProvider - { - /// - /// Creates a maling from a template - /// - Task Create(string mailTemplateKey, Action onCreate = null); - - /// - /// Creates a maling from a template - /// - Task Create(string mailTemplateKey, Action onCreate = null) where T : Mail, new(); - - /// - /// Renders the message body. - /// This is automatically called when sending messages. - /// - Task Render(Mail message); - - /// - /// Sends a message with the default dispatcher - /// - Task Send(Mail message, CancellationToken token = default); - - /// - /// Sends a message with the specified dispatcher - /// - Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default); - } -} diff --git a/old/zero.Core/Mails/MailSendResult.cs b/old/zero.Core/Mails/MailSendResult.cs deleted file mode 100644 index ba0263e7..00000000 --- a/old/zero.Core/Mails/MailSendResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Linq; - -namespace zero.Core.Mails -{ - public class MailSendResult - { - public string Alias { get; set; } - - public DateTimeOffset LastRunDate { get; set; } - - public MailSendResult() { } - } -} diff --git a/old/zero.Core/Messages/IMessage.cs b/old/zero.Core/Messages/IMessage.cs deleted file mode 100644 index 667ccafc..00000000 --- a/old/zero.Core/Messages/IMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace zero.Core.Messages -{ - public interface IMessage - { - } -} diff --git a/old/zero.Core/Messages/IMessageHandler.cs b/old/zero.Core/Messages/IMessageHandler.cs deleted file mode 100644 index 1c323463..00000000 --- a/old/zero.Core/Messages/IMessageHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace zero.Core.Messages -{ - /// - /// Indicates a handler that can perform an action when a message of - /// a certain type is received. (could be an interface rather than concrete type) - /// - public interface IMessageHandler where TMessage : IMessage - { - /// - /// Method to invoke when a message of type TMessage is published - /// - Task Handle(TMessage message); - } - - - /// - /// Indicates a handler that can perform an action when a message of - /// a certain type is received. (could be an interface rather than concrete type) - /// - public interface IBatchMessageHandler : IMessageHandler where TMessage : IMessage - { - /// - /// Method to invoke when a batch of messages of type TMessage are published - /// - Task HandleBatchAsync(IReadOnlyCollection message); - } -} diff --git a/old/zero.Core/Messages/MessageAggregator.cs b/old/zero.Core/Messages/MessageAggregator.cs deleted file mode 100644 index 082005d8..00000000 --- a/old/zero.Core/Messages/MessageAggregator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace zero.Core.Messages -{ - public class MessageAggregator : IMessageAggregator - { - readonly ConcurrentBag Subscription = new ConcurrentBag(); - readonly IServiceProvider ServiceProvider; - - - public MessageAggregator(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - - - /// - public async Task Publish(TMessage message) where TMessage : class, IMessage - { - IEnumerable subscriptions = Subscription.Where(s => s.CanDeliver()); - - foreach (IMessageSubscription subscription in subscriptions) - { - await subscription.Deliver(ServiceProvider, message); - } - } - - - /// - public void Subscribe() - where TMessage : class, IMessage - where TMessageHandler : IMessageHandler - { - Subscription.Add(new MessageSubscription()); - } - } - - - public interface IMessageAggregator - { - /// - /// Publishes the specified message, invoking any handlers subscribed to the message. - /// - Task Publish(TMessage message) where TMessage : class, IMessage; - - /// - /// Subscribes the specified handler to the spified message type - /// - void Subscribe() - where TMessage : class, IMessage - where TMessageHandler : IMessageHandler; - } -} diff --git a/old/zero.Core/Messages/MessageSubscription.cs b/old/zero.Core/Messages/MessageSubscription.cs deleted file mode 100644 index bbce2ee0..00000000 --- a/old/zero.Core/Messages/MessageSubscription.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace zero.Core.Messages -{ - internal class MessageSubscription : IMessageSubscription - where TMessage : class, IMessage - where TMessageHandler : IMessageHandler - { - public bool CanDeliver() - { - return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T)); - } - - public async Task Deliver(IServiceProvider serviceProvider, object message) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - if (!(message is TMessage)) - { - throw new ArgumentException($"{ nameof(message) } must be of type '{typeof(TMessage).FullName}'"); - } - - var handler = serviceProvider.GetRequiredService(); - await handler.Handle((TMessage)message); - } - } - - - public interface IMessageSubscription - { - bool CanDeliver(); - - Task Deliver(IServiceProvider serviceProvider, object message); - } -} diff --git a/old/zero.Core/Paths.cs b/old/zero.Core/Paths.cs deleted file mode 100644 index 665f35eb..00000000 --- a/old/zero.Core/Paths.cs +++ /dev/null @@ -1,224 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.StaticFiles; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace zero.Core -{ - public class Paths : IPaths - { - public string WebRoot { get; set; } - - public string ContentRoot { get; set; } - - public string SecureRoot { get; set; } - - public string Media { get; set; } - - protected IWebHostEnvironment Env { get; set; } - - private bool IsDebug { get; set; } - - public const string MEDIA_FOLDER = "Uploads"; - - public const char PATH_SEPARATOR = '/'; - - private static char[] InvalidFilenameChars = null; - - private const char REPLACEMENT_CHAR = '-'; - - private FileExtensionContentTypeProvider FileExtensionContentTypeProvider { get; set; } - - private static Dictionary Replacements = new Dictionary() - { - { 'ä', "ae" }, - { 'ü', "ue" }, - { 'ö', "oe" }, - { 'ß', "ss" } - }; - - - public Paths(IWebHostEnvironment env, bool isDebug) - { - Env = env; - IsDebug = isDebug; - WebRoot = env.WebRootPath; - ContentRoot = env.ContentRootPath; - SecureRoot = Path.Combine(ContentRoot, "wwwroot.secure"); - Media = Path.Combine(WebRoot, MEDIA_FOLDER); - FileExtensionContentTypeProvider = new(); - } - - - /// - /// Combine a path - /// - public string Combine(params string[] paths) - { - return Path.Combine(paths); - } - - - /// - /// Map a path - /// - public string Map(string path) - { - return Path.Combine(WebRoot, path); - } - - - /// - /// Map a secure path - /// - public string MapSecure(string path) - { - return Path.Combine(SecureRoot, path); - } - - - /// - /// Map a path - /// - public string Map(params string[] paths) - { - return Path.Combine(WebRoot, Path.Combine(paths)); - } - - - /// - /// Map a secure path - /// - public string MapSecure(params string[] paths) - { - return Path.Combine(SecureRoot, Path.Combine(paths)); - } - - - /// - /// Create a directory if it does not exist yet - /// - public void Create(string directory) - { - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - } - - - /// - /// Get content type for a filename - /// - public string GetContentType(string filename, string fallback = "application/octet-stream") - { - if (filename == null || !FileExtensionContentTypeProvider.TryGetContentType(Path.GetFileName(filename), out string contentType)) - { - contentType = fallback; - } - return contentType; - } - - - /// - /// Normalizes a filename and removes invalid chars - /// - public string ToFilename(string value) - { - if (String.IsNullOrWhiteSpace(value)) - { - return value; - } - - // lowercase for string - value = value.ToLower(); - - StringBuilder sb = new StringBuilder(value.Length); - - var invalids = InvalidFilenameChars ?? (InvalidFilenameChars = Path.GetInvalidFileNameChars()); - bool changed = false; - - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - - if (Replacements.ContainsKey(c)) - { - changed = true; - sb.Append(Replacements[c]); - } - else if (invalids.Contains(c)) - { - changed = true; - sb.Append(REPLACEMENT_CHAR); - } - else - { - sb.Append(c); - } - } - - if (sb.Length == 0) - { - return "_"; - } - - return changed ? sb.ToString() : value; - } - } - - - public interface IPaths - { - string ContentRoot { get; set; } - - string WebRoot { get; set; } - - string SecureRoot { get; set; } - - string Media { get; set; } - - /// - /// Combine a path - /// - string Combine(params string[] paths); - - /// - /// Map a path - /// - string Map(string path); - - /// - /// Map a secure path - /// - string MapSecure(string path); - - /// - /// Map a path - /// - string Map(params string[] paths); - - /// - /// Map a secure path - /// - string MapSecure(params string[] paths); - - /// - /// Create a directory if it does not exist yet - /// - void Create(string directory); - - /// - /// Get content type for a filename - /// - string GetContentType(string filename, string fallback = "application/octet-stream"); - - /// - /// Normalizes a filename and removes invalid chars - /// - string ToFilename(string value); - } -} diff --git a/old/zero.Core/Plugins/IZeroBuiltInPlugin.cs b/old/zero.Core/Plugins/IZeroBuiltInPlugin.cs deleted file mode 100644 index 6a7f43ab..00000000 --- a/old/zero.Core/Plugins/IZeroBuiltInPlugin.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using zero.Core.Options; - -namespace zero.Core.Plugins -{ - internal interface IZeroBuiltInPlugin - { - - } -} \ No newline at end of file diff --git a/old/zero.Core/Plugins/IZeroPlugin.cs b/old/zero.Core/Plugins/IZeroPlugin.cs deleted file mode 100644 index 881b0bb8..00000000 --- a/old/zero.Core/Plugins/IZeroPlugin.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using zero.Core.Options; - -namespace zero.Core.Plugins -{ - public abstract class ZeroPlugin : IZeroPlugin - { - public IZeroPluginOptions Options { get; } = new ZeroPluginOptions(); - - public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { } - - public virtual void Configure(IZeroOptions zero) { } - } - - - public interface IZeroPlugin - { - IZeroPluginOptions Options { get; } - - void ConfigureServices(IServiceCollection services, IConfiguration configuration); - - void Configure(IZeroOptions zero); - } -} \ No newline at end of file diff --git a/old/zero.Core/Plugins/ZeroPluginOptions.cs b/old/zero.Core/Plugins/ZeroPluginOptions.cs deleted file mode 100644 index 56882313..00000000 --- a/old/zero.Core/Plugins/ZeroPluginOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace zero.Core.Plugins -{ - public class ZeroPluginOptions : IZeroPluginOptions - { - public string Name { get; set; } - - public string Description { get; set; } - - public List LocalizationPaths { get; private set; } = new List(); - - public string PluginPath { get; set; } - } - - - public interface IZeroPluginOptions - { - string Name { get; set; } - - string Description { get; set; } - - List LocalizationPaths { get; } - - string PluginPath { get; set; } - } - - - public interface IZeroPluginStartup - { - Task Startup(); - } -} diff --git a/old/zero.Core/Renderer/RazorRenderer.cs b/old/zero.Core/Renderer/RazorRenderer.cs deleted file mode 100644 index ce51aa1b..00000000 --- a/old/zero.Core/Renderer/RazorRenderer.cs +++ /dev/null @@ -1,287 +0,0 @@ -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewEngines; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Encodings.Web; -using System.Threading.Tasks; - -namespace zero.Core.Renderer -{ - public class RazorRenderer : IRazorRenderer, IDisposable - { - protected IRazorViewEngine ViewEngine { get; set; } - - protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; } - - protected IModelMetadataProvider ModelMetadataProvider { get; set; } - - protected IHttpContextAccessor HttpContextAccessor { get; set; } - - protected IServiceScope ServiceScope { get; set; } - - protected HtmlHelperOptions HtmlHelperOptions { get; set; } - - - public RazorRenderer(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory, - IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions mvcHelperOptions) - { - ViewEngine = viewEngine; - HttpContextAccessor = httpContextAccessor; - TempDataDictionaryFactory = tempDataDictionaryFactory; - ModelMetadataProvider = modelMetadataProvider; - ServiceScope = serviceProvider.CreateScope(); - HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions; - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(object args = null) where T : ViewComponent - { - return await ComponentAsync(typeof(T), args); - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(Type componentType, object args = null) - { - return await ComponentAsync(componentType, BuildActionContext(), args); - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(string componentName, object args = null) - { - return await ComponentAsync(componentName, BuildActionContext(), args); - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent - { - return await ComponentAsync(typeof(T), context, args); - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(Type componentType, ActionContext context, object args = null) - { - IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); - - using StringWriter stringWriter = new(); - - ViewContext viewContext = BuildViewContext(context, stringWriter); - - (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); - IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args); - - result.WriteTo(stringWriter, HtmlEncoder.Default); - await stringWriter.FlushAsync(); - return stringWriter.ToString(); - } - - - /// - /// Renders a razor component to a string - /// - public async Task ComponentAsync(string componentName, ActionContext context, object args = null) - { - IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); - - using StringWriter stringWriter = new(); - - ViewContext viewContext = BuildViewContext(context, stringWriter); - - (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); - IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args); - - result.WriteTo(stringWriter, HtmlEncoder.Default); - await stringWriter.FlushAsync(); - return stringWriter.ToString(); - } - - - /// - /// Renders a razor view to a string - /// - public async Task ViewAsync(string view, object model = null) - { - return await ViewAsync(BuildActionContext(), view, model); - } - - - /// - /// Renders a razor view to a string - /// - public async Task ViewAsync(ActionContext context, string view, object model = null) - { - IView viewResult = FindView(context, view); - - using StringWriter stringWriter = new(); - - ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult); - viewContext.RouteData = context.RouteData; - viewContext.ViewData.Model = model; - - await viewResult.RenderAsync(viewContext); - await stringWriter.FlushAsync(); - - return stringWriter.ToString(); - } - - - /// - public void Dispose() - { - ServiceScope?.Dispose(); - } - - - /// - /// Build the view context - /// - protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null) - { - ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...; - ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...; - - return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions); - } - - - /// - /// Builds a new view context - /// - protected virtual ActionContext BuildActionContext() - { - HttpContext context = GetHttpContext(); - RouteData routeData = context.GetRouteData(); - return new ActionContext(context, routeData, new ActionDescriptor()); - } - - - /// - /// Get HTTP context or mock one - /// - protected virtual HttpContext GetHttpContext() - { - HttpContext context = HttpContextAccessor.HttpContext; - context ??= new DefaultHttpContext() - { - RequestServices = ServiceScope.ServiceProvider - }; - - return context; - } - - - /// - /// Tries to find a view - /// - protected virtual IView FindView(ActionContext actionContext, string viewName) - { - ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false); - if (getViewResult.Success) - { - return getViewResult.View; - } - - ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false); - if (findViewResult.Success) - { - return findViewResult.View; - } - - IEnumerable searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations); - string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations)); - - throw new InvalidOperationException(errorMessage); - } - } - - - public interface IRazorRenderer - { - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(object args = null) where T : ViewComponent; - - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(Type componentType, object args = null); - - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(string componentName, object args = null); - - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent; - - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(Type componentType, ActionContext context, object args = null); - - /// - /// Renders a razor component to a string - /// - Task ComponentAsync(string componentName, ActionContext context, object args = null); - - /// - /// Renders a razor view to a string - /// - Task ViewAsync(string view, object model = null); - - /// - /// Renders a razor view to a string - /// - Task ViewAsync(ActionContext context, string view, object model = null); - } - - - class GenericController : ControllerBase { } - - - class NullView : IView - { - public static readonly NullView Instance = new NullView(); - - public string Path => string.Empty; - - public Task RenderAsync(ViewContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return Task.CompletedTask; - } - } -} diff --git a/old/zero.Core/Renderer/TokenReplacement.cs b/old/zero.Core/Renderer/TokenReplacement.cs deleted file mode 100644 index 88e6f0c6..00000000 --- a/old/zero.Core/Renderer/TokenReplacement.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using zero.Core.Extensions; - -namespace zero.Core.Renderer -{ - public class TokenReplacement - { - static Regex TokenRegex; - - - static TokenReplacement() - { - TokenRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase); - } - - - public static string Apply(string text, Dictionary tokens) - { - if (text.IsNullOrWhiteSpace()) - { - return text; - } - - MatchCollection matches = TokenRegex.Matches(text); - - foreach (Match match in matches) - { - if (!match.Success) - { - continue; - } - - string original = match.Value; - string token = match.Groups[1].Value; - - if (tokens.ContainsKey(token)) - { - text = text.Replace(original, tokens[token]); - } - } - - return text; - - //foreach ((string key, string value) in tokens) - //{ - // string tokenKey = key.EnsureStartsWith(BEGIN).EnsureEndsWith(END); - // string tokenValue = value; // TODO escape for HTML? - - // text = text.Replace(tokenKey, value); - //} - - //return text; - } - } -} diff --git a/old/zero.Core/Services/Localizer.cs b/old/zero.Core/Services/Localizer.cs deleted file mode 100644 index 2dedc8f1..00000000 --- a/old/zero.Core/Services/Localizer.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using zero.Core.Attributes; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Renderer; - -namespace zero.Core.Services -{ - public class Localizer : ILocalizer - { - protected Dictionary Cache { get; private set; } = new(); - - protected IZeroStore Store { get; private set; } - - - public Localizer(IZeroStore store) - { - Store = store; - } - - - /// - public string Text(string key) => Text(key, null); - - - /// - public string Text(string key, Dictionary tokens) - { - if (key.IsNullOrEmpty()) - { - return null; - } - - if (!Cache.TryGetValue(key, out string value)) - { - Translation translation = LoadTranslation(key); - - if (translation == null) - { - return null; - } - - value = translation.Value; - - lock (Cache) // TOOD use concurrent dictionary - { - Cache[key] = value; - } - } - - if (tokens != null) - { - value = TokenReplacement.Apply(value, tokens); - } - - return value; - } - - - /// - public string Text(T enumValue) where T : Enum => Text(enumValue, null); - - - /// - public string Text(T enumValue, Dictionary tokens) where T : Enum - { - Type type = enumValue.GetType(); - MemberInfo memInfo = type.GetMember(enumValue.ToString())[0]; - return Text(memInfo.GetCustomAttribute()?.Key, tokens); - } - - - /// - public string Maybe(string key) => Maybe(key, null); - - - /// - public string Maybe(string key, Dictionary tokens) - { - return key.IsNullOrEmpty() || !key.StartsWith("@") ? key : Text(key.Substring(1), tokens); - } - - - /// - /// Get translation from database or any other source - /// - protected virtual Translation LoadTranslation(string key) - { - return Store.Session().Query().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^ - } - } - - public interface ILocalizer - { - /// - /// - /// - string Text(string key); - - /// - /// - /// - string Text(string key, Dictionary tokens); - - /// - /// Get a text string from a [Localize] attribute - /// - string Text(T enumValue) where T : Enum; - - /// - /// Get a text string from a [Localize] attribute - /// - string Text(T enumValue, Dictionary tokens) where T : Enum; - - /// - /// Only tries to resolve the key when it is prefixed with an @ - /// - string Maybe(string key); - - /// - /// Only tries to resolve the key when it is prefixed with an @ - /// - string Maybe(string key, Dictionary tokens); - } -} diff --git a/old/zero.Core/Validation/ApplicationValidator.cs b/old/zero.Core/Validation/ApplicationValidator.cs deleted file mode 100644 index 0cb9d810..00000000 --- a/old/zero.Core/Validation/ApplicationValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentValidation; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Validation -{ - public class ApplicationValidator : ZeroValidator - { - public ApplicationValidator() - { - //RuleFor(x => x.Code).Length(2); - RuleFor(x => x.Name).NotEmpty().Length(2, 50); - RuleFor(x => x.FullName).MaximumLength(120); - RuleFor(x => x.Email).Email().NotEmpty().MaximumLength(120); - RuleFor(x => x.Domains).NotEmpty(); - } - } -} diff --git a/old/zero.Core/Validation/BackofficeUserValidator.cs b/old/zero.Core/Validation/BackofficeUserValidator.cs deleted file mode 100644 index 796781a4..00000000 --- a/old/zero.Core/Validation/BackofficeUserValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentValidation; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Validation -{ - public class BackofficeUserValidator : ZeroValidator - { - public BackofficeUserValidator(bool isCreate = false) - { - RuleFor(x => x.Name).Length(2, 80); - RuleFor(x => x.Email).NotEmpty().Email().MaximumLength(120); - RuleFor(x => x.PasswordHash).NotEmpty(); - RuleFor(x => x.LanguageId).NotEmpty(); - RuleFor(x => x.RoleIds).NotEmpty(); - - if (isCreate) - { - RuleFor(x => x.IsSuper).Equals(false); - } - } - } -} diff --git a/old/zero.Core/Validation/SetupModelValidator.cs b/old/zero.Core/Validation/SetupModelValidator.cs deleted file mode 100644 index 975dec7b..00000000 --- a/old/zero.Core/Validation/SetupModelValidator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentValidation; -using zero.Core.Entities.Setup; -using zero.Core.Extensions; - -namespace zero.Core.Validation -{ - public class SetupModelValidator : ZeroValidator - { - public SetupModelValidator() - { - RuleFor(x => x.User).NotNull(); - RuleFor(x => x.User.Email).NotEmpty().Email(); - RuleFor(x => x.User.Name).MaximumLength(40).NotEmpty(); - RuleFor(x => x.User.Password).MaximumLength(1024); // TODO password policy - - RuleFor(x => x.AppName).MaximumLength(40).NotEmpty(); - - RuleFor(x => x.Database.Url).NotEmpty().Url(); - RuleFor(x => x.Database.Name).NotEmpty(); - } - } -} diff --git a/old/zero.Core/Validation/UserRoleValidator.cs b/old/zero.Core/Validation/UserRoleValidator.cs deleted file mode 100644 index e0b84f84..00000000 --- a/old/zero.Core/Validation/UserRoleValidator.cs +++ /dev/null @@ -1,40 +0,0 @@ -using FluentValidation; -using zero.Core.Entities; -using zero.Core.Extensions; -using System.Linq; -using System; - -namespace zero.Core.Validation -{ - public class UserRoleValidator : ZeroValidator - { - const string SECTION_CLAIM = "section."; - - const string TRUE_CLAIM_VALUE = ":true"; - - public UserRoleValidator() - { - RuleFor(x => x.Name).Length(2, 80); - RuleFor(x => x.Description).MaximumLength(200); - RuleFor(x => x.Icon).NotEmpty(); - - RuleFor(x => x.Claims).NotEmpty().Must((role, claims, context) => - { - foreach (UserClaim claim in claims) - { - if (claim.Value.StartsWith(SECTION_CLAIM, StringComparison.InvariantCultureIgnoreCase) && claim.Value.EndsWith(TRUE_CLAIM_VALUE, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - } - - return false; - }).WithMessage("@errors.role.nosection"); - - RuleForEach(x => x.Claims).Must((role, claim, context) => - { - return !claim.Type.IsNullOrEmpty() && !claim.Value.IsNullOrEmpty(); - }).WithMessage("@errors.role.emptyclaim"); - } - } -} diff --git a/old/zero.Core/ZeroContext.cs b/old/zero.Core/ZeroContext.cs deleted file mode 100644 index 7eb18a35..00000000 --- a/old/zero.Core/ZeroContext.cs +++ /dev/null @@ -1,275 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Raven.Client.Documents.Session; -using System; -using System.Collections.Concurrent; -using System.Security.Claims; -using System.Threading.Tasks; -using zero.Core.Cultures; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Handlers; -using zero.Core.Options; -using zero.Core.Routing; -using zero.Core.Utils; - -namespace zero.Core -{ - public class ZeroContext : IZeroContext - { - /// - public Application Application { get; protected set; } - - /// - public string AppId { get; protected set; } - - /// - public ClaimsPrincipal BackofficeUser { get; protected set; } - - /// - public bool IsBackofficeRequest { get; protected set; } - - /// - public bool IsLoggedIntoBackoffice { get; protected set; } - - /// - public IZeroOptions Options { get; protected set; } - - /// - public Route Route => ResolvedRoute?.Route; - - /// - public IRouteModel ResolvedRoute => HttpContextAccessor?.HttpContext?.Features.Get(); - - /// - public IZeroStore Store { get; private set; } - - /// - public IServiceProvider Services { get; private set; } - - - protected IApplicationResolver AppResolver { get; private set; } - - protected ICultureResolver CultureResolver { get; private set; } - - protected ILogger Logger { get; private set; } - - protected IHandlerHolder Handler { get; private set; } - - protected IHttpContextAccessor HttpContextAccessor { get; private set; } - - protected IPrimitiveTypeCollection ValueCollection { get; private set; } - - - bool _resolved = false; - - - public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, IApplicationResolver appResolver, ICultureResolver cultureResolver, - ILogger logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services) - { - Options = options; - AppResolver = appResolver; - CultureResolver = cultureResolver; - Logger = logger; - Store = store; - Handler = handler; - ValueCollection = new PrimitiveTypeCollection(); - HttpContextAccessor = httpContextAccessor; - Services = services; - } - - - /// - public async virtual Task Resolve(HttpContext context) - { - if (_resolved) - { - return; - } - - if (context?.Request is null) - { - Store.ResolvedDatabase = null; - return; - } - - if (!Options.SetupCompleted) - { - return; - } - - _resolved = true; - - // check if the current request is a backoffice request - IsBackofficeRequest = context.IsBackofficeRequest(Options.BackofficePath); - - // get the currently logged-in backoffice user - BackofficeUser = new ClaimsPrincipal(); - IsLoggedIntoBackoffice = false; - AuthenticateResult authResult = await context.AuthenticateAsync(Constants.Auth.BackofficeScheme); - if (authResult?.Principal is not null) - { - BackofficeUser = authResult.Principal; - IsLoggedIntoBackoffice = true; - } - - // resolve current application - Application = await AppResolver.Resolve(context, BackofficeUser); - AppId = Application.Id; - - // set default database for document store - Store.ResolvedDatabase = Application.Database; - - // set current culture - await CultureResolver.Resolve(this); - } - - - /// - public void Override(Application app) - { - Application = app; - AppId = app?.Id; - Store.ResolvedDatabase = app?.Database; - } - - - /// - public ZeroContextScope CreateScope(Application app) - { - return new(this, app, Application); - } - - - /// - public T Get() => ValueCollection.Get(); - - - /// - public void Set(T value) => ValueCollection.Set(value); - - - /// - public void Remove() => ValueCollection.Remove(); - } - - - - public class ZeroContextScope : IDisposable - { - public IZeroContext Context { get; } - - public Application App { get; set; } - - public string Database { get; set; } - - public IZeroStore Store { get; set; } - - Application _originalApp = null; - - - internal ZeroContextScope(IZeroContext context, Application app, Application originalApp) - { - Context = context; - App = app; - Database = app.Database; - Store = context.Store; - _originalApp = originalApp; - Context.Override(app); - } - - /// - public void Dispose() - { - Context.Override(_originalApp); - } - } - - - - public interface IZeroContext - { - /// - /// Currently loaded application - /// - Application Application { get; } - - /// - /// Current loaded application Id - /// - string AppId { get; } - - /// - /// Resolved backoffice user principal - /// - ClaimsPrincipal BackofficeUser { get; } - - /// - /// Whether the current request is a backoffice request or not - /// - bool IsBackofficeRequest { get; } - - /// - /// Whether the user is logged into the backoffice - /// - bool IsLoggedIntoBackoffice { get; } - - /// - /// Global zero options - /// - IZeroOptions Options { get; } - - /// - /// Document store - /// - IZeroStore Store { get; } - - /// - /// Service container - /// - IServiceProvider Services { get; } - - /// - /// Matching (frontend) path route - /// - Route Route { get; } - - /// - /// Matching (frontend) resolved route - /// - IRouteModel ResolvedRoute { get; } - - /// - /// Resolves the current application (for backoffice + frontend requests) and - /// the currently active backoffice user, as users are not signed in with the default scheme and do therefore not populate HttpContext.User - /// - Task Resolve(HttpContext context); - - /// - /// Overrides the resolved application for this context instance - /// - void Override(Application app); - - /// - /// SCOPE - /// - ZeroContextScope CreateScope(Application app); - - /// - /// Get a custom property from this scoped context - /// - T Get(); - - /// - /// Add a custom property to this scoped context - /// - void Set(T value); - - /// - /// Remove a custom property from this scoped context - /// - void Remove(); - } -} diff --git a/old/zero.Web/Controllers/CountriesController.cs b/old/zero.Web/Controllers/CountriesController.cs deleted file mode 100644 index 1ec50f5d..00000000 --- a/old/zero.Web/Controllers/CountriesController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Database.Indexes; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Identity; - -namespace zero.Web.Controllers -{ - [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)] - public class CountriesController : ZeroBackofficeCollectionController - { - public CountriesController(ICountriesCollection collection) : base(collection) - { - PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant(); - } - - public override Task> GetByQuery([FromQuery] ListBackofficeQuery query) - { - query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); - return Collection.Load(query); - } - } -} diff --git a/old/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs b/old/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs deleted file mode 100644 index 66906d28..00000000 --- a/old/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.DependencyModel; -using System; -using zero.Core.Assemblies; - -namespace zero.Web.Defaults -{ - public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule - { - const string ZERO_PREFIX = "zero."; - - /// - public bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context) - { - StringComparison casing = StringComparison.OrdinalIgnoreCase; - // TODO we need to auto-add assemblies and discover their types which have implementations of IZeroPlugin - return library.Name.StartsWith(ZERO_PREFIX, casing) || (context.HasEntryAssembly && library.Name.Contains(context.EntryAssemblyName, casing)); - } - } -} diff --git a/old/zero.Web/Defaults/ZeroBackofficePlugin.cs b/old/zero.Web/Defaults/ZeroBackofficePlugin.cs index 65033152..845f4342 100644 --- a/old/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/old/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -1,23 +1,6 @@ -using FluentValidation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using zero.Core; -using zero.Core.Api; -using zero.Core.Blueprints; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Integrations; -using zero.Core.Mails; -using zero.Core.Messages; -using zero.Core.Options; -using zero.Core.Plugins; -using zero.Core.Renderer; -using zero.Core.Routing; -using zero.Core.Services; -using zero.Core.Tokens; -using zero.Core.Validation; using zero.Web.Sections; using zero.Web.ViewHelpers; @@ -61,22 +44,6 @@ namespace zero.Web.Defaults { //services.AddAll(typeof(IValidator<>), ServiceLifetime.Scoped); //services.AddAll(typeof(IValidator), ServiceLifetime.Scoped); - services.AddSingleton(); - - services.AddScoped(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddTransient, ApplicationValidator>(); - services.AddTransient, TranslationValidator>(); - services.AddTransient, PageValidator>(); - services.AddTransient, UserRoleValidator>(); - services.AddTransient, BackofficeUserValidator>(); services.AddTransient(); services.AddTransient(); @@ -98,23 +65,7 @@ namespace zero.Web.Defaults services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - services.AddScoped(); - services.AddScoped(); + services.AddTransient(); services.AddScoped(); @@ -125,16 +76,7 @@ namespace zero.Web.Defaults services.AddScoped(typeof(ICollectionContext<>), typeof(CollectionContext<>)); services.AddScoped(typeof(IInterceptorRunner<>), typeof(InterceptorRunner<>)); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); } } } \ No newline at end of file diff --git a/old/zero.Web/DevServer/EventedStreamReader.cs b/old/zero.Web/DevServer/EventedStreamReader.cs deleted file mode 100644 index ddbf6656..00000000 --- a/old/zero.Web/DevServer/EventedStreamReader.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) .NET Foundation Contributors. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Original Source: https://github.com/aspnet/JavaScriptServices - -using System; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - - -namespace Zero.Web.DevServer -{ - /// - /// Wraps a to expose an evented API, issuing notifications - /// when the stream emits partial lines, completed lines, or finally closes. - /// - internal class EventedStreamReader - { - public delegate void OnReceivedChunkHandler(ArraySegment chunk); - public delegate void OnReceivedLineHandler(string line); - public delegate void OnStreamClosedHandler(); - - public event OnReceivedChunkHandler OnReceivedChunk; - public event OnReceivedLineHandler OnReceivedLine; - public event OnStreamClosedHandler OnStreamClosed; - - private readonly StreamReader _streamReader; - private readonly StringBuilder _linesBuffer; - - public EventedStreamReader(StreamReader streamReader) - { - _streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader)); - _linesBuffer = new StringBuilder(); - Task.Factory.StartNew(Run); - } - - public Task WaitForMatch(Regex regex) - { - var tcs = new TaskCompletionSource(); - var completionLock = new object(); - - OnReceivedLineHandler onReceivedLineHandler = null; - OnStreamClosedHandler onStreamClosedHandler = null; - - void ResolveIfStillPending(Action applyResolution) - { - lock (completionLock) - { - if (!tcs.Task.IsCompleted) - { - OnReceivedLine -= onReceivedLineHandler; - OnStreamClosed -= onStreamClosedHandler; - applyResolution(); - } - } - } - - onReceivedLineHandler = line => - { - var match = regex.Match(line); - if (match.Success) - { - ResolveIfStillPending(() => tcs.SetResult(match)); - } - }; - - onStreamClosedHandler = () => - { - ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException())); - }; - - OnReceivedLine += onReceivedLineHandler; - OnStreamClosed += onStreamClosedHandler; - - return tcs.Task; - } - - - public Task WaitForFinish() - { - var tcs = new TaskCompletionSource(); - var completionLock = new object(); - - OnStreamClosedHandler onStreamClosedHandler = null; - - void ResolveIfStillPending(Action applyResolution) - { - lock (completionLock) - { - if (!tcs.Task.IsCompleted) - { - OnStreamClosed -= onStreamClosedHandler; - applyResolution(); - } - } - } - - onStreamClosedHandler = () => - { - ResolveIfStillPending(() => tcs.SetResult()); - }; - - OnStreamClosed += onStreamClosedHandler; - - return tcs.Task; - } - - - private async Task Run() - { - var buf = new char[8 * 1024]; - while (true) - { - var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length); - if (chunkLength == 0) - { - OnClosed(); - break; - } - - OnChunk(new ArraySegment(buf, 0, chunkLength)); - - int lineBreakPos = -1; - int startPos = 0; - - // get all the newlines - while ((lineBreakPos = Array.IndexOf(buf, '\n', startPos, chunkLength - startPos)) >= 0 && startPos < chunkLength) - { - var length = lineBreakPos - startPos; - _linesBuffer.Append(buf, startPos, length); - OnCompleteLine(_linesBuffer.ToString()); - _linesBuffer.Clear(); - startPos = lineBreakPos + 1; - } - - // get the rest - if (lineBreakPos < 0 && startPos < chunkLength) - { - _linesBuffer.Append(buf, startPos, chunkLength - startPos); - } - } - } - - private void OnChunk(ArraySegment chunk) - { - var dlg = OnReceivedChunk; - dlg?.Invoke(chunk); - } - - private void OnCompleteLine(string line) - { - var dlg = OnReceivedLine; - dlg?.Invoke(line); - } - - private void OnClosed() - { - var dlg = OnStreamClosed; - dlg?.Invoke(); - } - } - - /// - /// Captures the completed-line notifications from a , - /// combining the data into a single . - /// - internal class EventedStreamStringReader : IDisposable - { - private EventedStreamReader _eventedStreamReader; - private bool _isDisposed; - private StringBuilder _stringBuilder = new StringBuilder(); - - public EventedStreamStringReader(EventedStreamReader eventedStreamReader) - { - _eventedStreamReader = eventedStreamReader - ?? throw new ArgumentNullException(nameof(eventedStreamReader)); - _eventedStreamReader.OnReceivedLine += OnReceivedLine; - } - - public string ReadAsString() => _stringBuilder.ToString(); - - private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line); - - public void Dispose() - { - if (!_isDisposed) - { - _eventedStreamReader.OnReceivedLine -= OnReceivedLine; - _isDisposed = true; - } - } - } -} diff --git a/old/zero.Web/DevServer/PidUtils.cs b/old/zero.Web/DevServer/PidUtils.cs deleted file mode 100644 index 1f2604b7..00000000 --- a/old/zero.Web/DevServer/PidUtils.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Zero.Web.DevServer -{ - public static class PidUtils - { - const string ssPidRegex = @"(?:^|"",|"",pid=)(\d+)"; - const string portRegex = @"[^]*[.:](\\d+)$"; - - public static int GetPortPid(ushort port) - { - int pidOut = -1; - - int portColumn = 1; // windows - int pidColumn = 4; // windows - string pidRegex = null; - - List results = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - results = RunProcessReturnOutputSplit("netstat", "-anv -p tcp"); - results.AddRange(RunProcessReturnOutputSplit("netstat", "-anv -p udp")); - portColumn = 3; - pidColumn = 8; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - results = RunProcessReturnOutputSplit("ss", "-tunlp"); - portColumn = 4; - pidColumn = 6; - pidRegex = ssPidRegex; - } - else - { - results = RunProcessReturnOutputSplit("netstat", "-ano"); - } - - - foreach (var line in results) - { - if (line.Length <= portColumn || line.Length <= pidColumn) continue; - try - { - // split lines to words - var portMatch = Regex.Match(line[portColumn], $"[.:]({port})"); - if (portMatch.Success) - { - int portValue = int.Parse(portMatch.Groups[1].Value); - - if (pidRegex == null) - { - pidOut = int.Parse(line[pidColumn]); - return pidOut; - } - else - { - var pidMatch = Regex.Match(line[pidColumn], pidRegex); - if (pidMatch.Success) - { - pidOut = int.Parse(pidMatch.Groups[1].Value); - } - } - } - } - catch (Exception) - { - // ignore line error - } - } - - return pidOut; - } - - private static List RunProcessReturnOutputSplit(string fileName, string arguments) - { - string result = RunProcessReturnOutput(fileName, arguments); - if (result == null) return new List(); - - string[] lines = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - var lineWords = new List(); - foreach (var line in lines) - { - lineWords.Add(line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); - } - return lineWords; - } - - private static string RunProcessReturnOutput(string fileName, string arguments) - { - Process process = null; - try - { - var si = new ProcessStartInfo(fileName, arguments) - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - process = Process.Start(si); - var stdOutT = process.StandardOutput.ReadToEndAsync(); - var stdErrorT = process.StandardError.ReadToEndAsync(); - if (!process.WaitForExit(10000)) - { - try { process?.Kill(); } catch { } - } - - if (Task.WaitAll(new Task[] { stdOutT, stdErrorT }, 10000)) - { - // if success, return data - return (stdOutT.Result + Environment.NewLine + stdErrorT.Result).Trim(); - } - return null; - } - catch (Exception) - { - return null; - } - finally - { - process?.Close(); - } - } - - public static bool Kill(string process, bool ignoreCase = true, bool force = false, bool tree = true) - { - var args = new List(); - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - if (force) { args.Add("-9"); } - if (ignoreCase) { args.Add("-i"); } - args.Add(process); - RunProcessReturnOutput("pkill", string.Join(" ", args)); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - if (force) { args.Add("-9"); } - if (ignoreCase) { args.Add("-I"); } - args.Add(process); - RunProcessReturnOutput("killall", string.Join(" ", args)); - } - else - { - if (force) { args.Add("/f"); } - if (tree) { args.Add("/T"); } - args.Add("/im"); - args.Add(process); - return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false; - } - return true; - } - catch (Exception) - { - - } - return false; - } - - public static bool Kill(int pid, bool force = false, bool tree = true) - { - if (pid == -1) { return false; } - - var args = new List(); - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - if (force) { args.Add("-9"); } - args.Add(pid.ToString()); - RunProcessReturnOutput("kill", string.Join(" ", args)); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - if (force) { args.Add("-9"); } - args.Add(pid.ToString()); - RunProcessReturnOutput("kill", string.Join(" ", args)); - } - else - { - if (force) { args.Add("/f"); } - if (tree) { args.Add("/T"); } - args.Add("/PID"); - args.Add(pid.ToString()); - return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false; - } - return true; - } - catch (Exception) - { - } - return false; - } - - public static bool KillPort(ushort port, bool force = false, bool tree = true) => Kill(GetPortPid(port), force: force, tree: tree); - - } -} diff --git a/old/zero.Web/DevServer/PluginResolver.cs b/old/zero.Web/DevServer/PluginResolver.cs deleted file mode 100644 index 1be22366..00000000 --- a/old/zero.Web/DevServer/PluginResolver.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using zero.Core.Plugins; - -namespace Zero.Web.DevServer -{ - public class PluginResolver - { - public IEnumerable Plugins { get; set; } - - - public PluginResolver(IEnumerable plugins) - { - Plugins = plugins; - } - } -} diff --git a/old/zero.Web/DevServer/ProcessProxy.cs b/old/zero.Web/DevServer/ProcessProxy.cs deleted file mode 100644 index 497b0119..00000000 --- a/old/zero.Web/DevServer/ProcessProxy.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace Zero.Web.DevServer -{ - public class ProcessProxy - { - string workingDirectory = null; - string script = null; - Action onProcessConfigure = null; - Action captureLog = null; - bool isWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - Dictionary envVars = new(); - HashSet arguments = new(); - bool forwardLog = false; - - Process process = null; - EventedStreamReader stdOut = null; - EventedStreamReader stdErr = null; - - - - - public ProcessProxy(string workingDirectory, string script, bool forwardLog = false) - { - this.workingDirectory = workingDirectory; - this.script = script; - this.forwardLog = forwardLog; - } - - - /// - /// Adds an environment variable - /// - public ProcessProxy EnvVar(string key, string value) - { - envVars.Add(key, value); - return this; - } - - - /// - /// Adds an argument - /// - public ProcessProxy Argument(string argument) - { - arguments.Add(argument); - return this; - } - - - /// - /// Configures the process start info - /// - public ProcessProxy Configure(Action onProcessConfigure) - { - this.onProcessConfigure = onProcessConfigure; - return this; - } - - - /// - /// Capture the log instead of outputting it to the console - /// - public ProcessProxy Capture(Action action) - { - this.captureLog = action; - return this; - } - - - /// - /// Run the script and wait for completion. - /// This is only recommended for scripts which finish automatically and have no user interaction. - /// - public async Task ExecuteAsync(TimeSpan timeout = default) - { - StartProcess(); - - using var stdErrReader = new EventedStreamStringReader(stdErr); - try - { - // TODO implement timeout - // https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource - await stdOut.WaitForFinish(); - } - catch (EndOfStreamException ex) - { - throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); - } - } - - - /// - /// Run the script and return as soon as a condition is met. - /// The script will not cancel and will continue to run as a sub-process as long as it does not auto-close. - /// - public async Task RunAsync(string startupCondition, TimeSpan startupTimeout = default) - { - StartProcess(); - - using var stdErrReader = new EventedStreamStringReader(stdErr); - try - { - await stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout)); - } - catch (EndOfStreamException ex) - { - throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); - } - } - - - public void Exit() - { - try { process?.Kill(); } catch { } - try { process?.WaitForExit(); } catch { } - - AppDomain.CurrentDomain.DomainUnload -= UnloadHandler; - AppDomain.CurrentDomain.ProcessExit -= UnloadHandler; - AppDomain.CurrentDomain.UnhandledException -= UnloadHandler; - } - - - /// - /// Run - /// - Process StartProcess() - { - string executable = script; - - StringBuilder command = new(); - command.Append(script); - command.Append(' '); - foreach (string arg in arguments) - { - command.Append(arg); - } - string argumentList = command.ToString(); - - if (isWindows) - { - argumentList = $"/c {argumentList}"; - executable = "cmd"; - } - - ProcessStartInfo startInfo = new(executable) - { - Arguments = argumentList, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - WorkingDirectory = workingDirectory - }; - - foreach (var envVar in envVars) - { - startInfo.Environment[envVar.Key] = envVar.Value; - } - - onProcessConfigure?.Invoke(startInfo); - - try - { - process = Process.Start(startInfo); - process.EnableRaisingEvents = true; - } - catch (Exception ex) - { - var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n" - + $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n" - + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n" - + " Make sure the executable is in one of those directories, or update your PATH.\n\n" - + "[2] See the InnerException for further details of the cause."; - throw new InvalidOperationException(message, ex); - } - - AttachLogger(); - - AppDomain.CurrentDomain.DomainUnload += UnloadHandler; - AppDomain.CurrentDomain.ProcessExit += UnloadHandler; - AppDomain.CurrentDomain.UnhandledException += UnloadHandler; - - return process; - } - - - void UnloadHandler(object sender, EventArgs e) - { - Exit(); - } - - - void AttachLogger(bool isStream = false) - { - stdOut = new EventedStreamReader(process.StandardOutput); - stdErr = new EventedStreamReader(process.StandardError); - - stdOut.OnReceivedLine += line => WriteToLog(line); - stdOut.OnReceivedChunk += chunk => WriteToLog(chunk); - stdErr.OnReceivedLine += line => WriteToLog(line, true); - stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true); - } - - - void WriteToLog(string line, bool isError = false) - { - if (String.IsNullOrWhiteSpace(line)) - { - return; - } - - line = line.StartsWith("") ? line.Substring(3) : line; - - if (captureLog != null) - { - captureLog(line, isError); - } - if (forwardLog) - { - (isError ? Console.Error : Console.Out).WriteLine(line); - } - } - - - void WriteToLog(ArraySegment chunk, bool isError = false) - { - bool containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0; - - if (containsNewline) - { - return; - } - - if (captureLog != null) - { - captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError); - } - if (forwardLog) - { - (isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count); - } - } - } -} diff --git a/old/zero.Web/DevServer/ZeroDevOptions.cs b/old/zero.Web/DevServer/ZeroDevOptions.cs deleted file mode 100644 index bb566ca1..00000000 --- a/old/zero.Web/DevServer/ZeroDevOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Zero.Web.DevServer -{ - public class ZeroDevOptions - { - public int Port { get; set; } = 3399; - - public bool ForwardLog { get; set; } = false; - - public string WorkingDirectory { get; set; } - - public bool Enabled { get; set; } = true; - } -} diff --git a/old/zero.Web/DevServer/ZeroDevService.cs b/old/zero.Web/DevServer/ZeroDevService.cs deleted file mode 100644 index 2741ce12..00000000 --- a/old/zero.Web/DevServer/ZeroDevService.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Plugins; - -namespace Zero.Web.DevServer -{ - public class ZeroDevService : IHostedService - { - IWebHostEnvironment env; - IOptions options; - ProcessProxy viteProcess; - ILogger logger; - PluginResolver pluginResolver; - string workingDirectory; - bool isRunning = false; - - - public ZeroDevService(IWebHostEnvironment env, IOptions options, ILogger logger, IEnumerable plugins) - { - //foreach (IZeroPlugin plugin in plugins) - //{ - // string location = Assembly.GetAssembly(plugin.GetType()). ; - //} - this.pluginResolver = new PluginResolver(plugins); - this.env = env; - this.options = options; - this.workingDirectory = options.Value.WorkingDirectory; - this.logger = logger; - } - - - public async Task StartAsync(CancellationToken cancellationToken) - { - // this is a development-time service, - // therefore no way to enable it in production - if (!env.IsDevelopment() || !options.Value.Enabled) - { - return; - } - - // locate npm version and throw if it is not installed - Version npmVersion = await FindNpmVersion(); - - if (npmVersion == null) - { - throw new Exception("Please install node+npm to use the zero dev service (https://www.npmjs.com/)"); - } - - // start vite server - viteProcess = await StartDevServer(options.Value.Port); - logger.LogInformation("vite listening on: http://localhost:{port}", options.Value.Port); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - - /// - /// Get the node version from the system - /// - async Task FindNpmVersion() - { - Version version = null; - - ProcessProxy process = new ProcessProxy(workingDirectory, "npm").Argument("-v").Capture((value, err) => - { - if (version == null && !value.Contains("not recognized") && Version.TryParse(value, out Version _version)) - { - version = _version; - } - }); - - await process.ExecuteAsync(); - - return version; - } - - - /// - /// Starts the vite dev server which also support HMR - /// - async Task StartDevServer(int port) - { - // if the port we want to use is occupied, terminate the process utilizing that port. - // this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process - PidUtils.KillPort((ushort)port, true); - - // get all plugins which need to be passed to vite - List plugins = new(); - - foreach (var plugin in pluginResolver.Plugins) - { - if (!String.IsNullOrEmpty(plugin.Options.PluginPath)) - { - plugins.Add(plugin.Options.PluginPath); - } - } - - // create and run the vite script - ProcessProxy process = new ProcessProxy(workingDirectory, "npm", options.Value.ForwardLog) - .Argument("run dev") - .EnvVar("PORT", port.ToString()) - .EnvVar("ZERO_PLUGINS", JsonConvert.SerializeObject(plugins)) - .Capture(CaptureLog); - - await process.RunAsync("localhost:", TimeSpan.FromMinutes(1)); - - isRunning = true; - - return process; - } - - - void CaptureLog(string line, bool isError) - { - if (!isRunning) - { - return; - } - - if (isError) - { - logger.LogWarning(line); - } - else - { - logger.LogInformation(line); - } - } - } -} diff --git a/old/zero.Web/Extensions/LocalizerExtensions.cs b/old/zero.Web/Extensions/LocalizerExtensions.cs deleted file mode 100644 index 7854c9d2..00000000 --- a/old/zero.Web/Extensions/LocalizerExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Html; -using System.Collections.Generic; -using zero.Core.Services; - -namespace zero.Web.Extensions -{ - public static class LocalizerExtensions - { - public static IHtmlContent Html(this ILocalizer localizer, string key) - { - string value = localizer.Text(key); - - HtmlContentBuilder builder = new(); - builder.SetHtmlContent(value); - return builder; - } - - - public static IHtmlContent Html(this ILocalizer localizer, string key, Dictionary tokens) - { - string value = localizer.Text(key, tokens); - - HtmlContentBuilder builder = new(); - builder.SetHtmlContent(value); - return builder; - } - } -} diff --git a/old/zero.Web/Formatters/RawJsonBodyInputFormatter.cs b/old/zero.Web/Formatters/RawJsonBodyInputFormatter.cs deleted file mode 100644 index 90f7c73e..00000000 --- a/old/zero.Web/Formatters/RawJsonBodyInputFormatter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Formatters; -using System; -using System.IO; -using System.Threading.Tasks; - -namespace zero.Web.Formatters -{ - public class RawJsonBodyInputFormatter : InputFormatter - { - public RawJsonBodyInputFormatter() - { - this.SupportedMediaTypes.Add("application/json"); - } - - public override async Task ReadRequestBodyAsync(InputFormatterContext context) - { - var request = context.HttpContext.Request; - using (var reader = new StreamReader(request.Body)) - { - var content = await reader.ReadToEndAsync(); - return await InputFormatterResult.SuccessAsync(content); - } - } - - protected override bool CanReadType(Type type) - { - return type == typeof(string); - } - } -} diff --git a/old/zero.Web/Resources/Localization/zero.de-de.json b/old/zero.Web/Resources/Localization/zero.de-de.json deleted file mode 100644 index a6dc4a9c..00000000 --- a/old/zero.Web/Resources/Localization/zero.de-de.json +++ /dev/null @@ -1,673 +0,0 @@ -{ - "zero": { - "name": "zero", - - "config": { - "linkareas": { - "pages": "Seiten", - "media": "Medien" - } - } - }, - - "ui": { - "yes": "Ja", - "no": "Nein", - "required": "Erforderlich", - "more": "Mehr", - "add": "Hinzufügen", - "actions": "Aktionen", - "back": "Zurück", - "close": "Schließen", - "confirm": "Bestätigen", - "save": "Speichern", - "create": "Erstellen", - "createdDate": "Erstellt am", - "modifiedDate": "Zuletzt geändert", - "id": "ID", - "disabled": "Deaktiviert", - "enabled": "Aktiv", - "disable": "Deaktivieren", - "enable": "Aktivieren", - "active": "Aktiv", - "inactive": "Inaktiv", - "delete": "Löschen", - "deleteQ": "Löschen?", - "default": "Standard", - "remove": "Entfernen", - "alias": "Alias", - "link": "Link", - "pagination": { - "xofy": "Seite {x} von {y}", - "next": "Nächste Seite", - "previous": "Vorherige Seite" - }, - "search": { - "placeholder": "Suchen ...", - "button": "Suchen" - }, - "name": "Name", - "select": "Select", - "icon": "Icon", - "icon_select": "Icon auswählen", - "tab_general": "Standard", - "tab_content": "Inhalte", - "tab_options": "Optionen", - "tab_infos": "Infos", - "pick": { - "title": "Auswählen", - "title_multiple": "Auswählen", - "title_autocomplete": "Wert eingeben", - "max": "max. {count}" - }, - "date": { - "set": "Datum setzen", - "select": "Datum auswählen...", - "xtoy": "{x} bis {y}", - "x": "ab {x}", - "y": "bis {y}", - "range_header": "Zeitspanne auswählen", - "range_from": "Datum von", - "range_to": "Datum bis" - }, - "price": { - "free": "Gratis", - "xtoy": "von {x} bis {y}", - "x": "ab {x}", - "y": "bis {y}", - "range_from": "Preis ab", - "range_to": "Preis bis" - }, - "sort": { - "title": "Sortieren", - "text": "Ziehe die verschiedenen Elemente wie gewünscht nach oben oder unten." - }, - "move": { - "title": "Verschieben", - "action": "Verschieben", - "text": "Wähle ein neues Überelement für {name} in der Baumstruktur unterhalb." - }, - "copy": { - "title": "Kopieren", - "action": "Kopieren", - "text": "Kopiere {name} an einen neuen Ort in der Baumstruktur unterhalb", - "includeDescendants": "Unterelemente inkludieren" - }, - "open": { - "title": "Öffnen" - }, - "rename": { - "title": "Umbenennen" - }, - "edit": { - "title": "Bearbeiten" - }, - "export": { - "action": "Exportieren" - }, - "parsedinput": { - "placeholder": "Wert eingeben", - "placeholder_output": "ID" - } - }, - - "gender": { - "undisclosed": "Ungenannt", - "male": "Männlich", - "female": "Weiblich", - "nonbinary": "Nicht-binär" - }, - - "revisions": { - "label": "Revisionen", - "actions": { - "updated": "Aktualisiert", - "created": "Erstellt", - "deleted": "Gelöscht" - }, - "view": "Ansehen" - }, - - "rte": { - "undo": "Rückgängig", - "redo": "Wiederholen", - "bold": "Fett", - "italic": "Kursiv", - "underline": "Unterstrichen", - "strikethrough": "Durchgestrichen", - "heading": "Überschrift", - "heading1": "Überschrift 1", - "heading2": "Überschrift 2", - "heading3": "Überschrift 3", - "heading4": "Überschrift 4", - "heading5": "Überschrift 5", - "heading6": "Überschrift 6", - "paragraph": "Absatz", - "quote": "Zitat", - "list": "Liste", - "olist": "Nummerierte Liste", - "ulist": "Ungeordnete Liste", - "code": "Code", - "line": "Linie", - "link": "Link", - "image": "Bild", - "video": "Video", - "accept": "OK", - "cancel": "Abbrechen", - "raw": "HTML bearbeiten" - }, - - "links": { - "providers": { - "page": "Seiten", - "media": "Medien" - } - }, - - "errors": { - "idnotfound": "Could not find an entity for the given ID", - "onsave": { - "notallowed": "You are not allowed to edit this resource", - "empty": "You need to pass a model" - }, - "ondelete": { - "idnotfound": "Could not find an entity for the given ID" - }, - "http": { - "403": "Unauthorized", - "403_text": "You are not allowed to view this resource", - "404": "Not found", - "404_text": "The requested resource could not be found
({path})", - "409": "Conflict", - "409_text": "The operation could not be completed as it is not valid anymore. Please reload the page", - "4xx": "Client error", - "4xx_text": "There was an internal client error (code: {code})", - "504": "Timed out", - "504_text": "The backoffice did not receive a timely response from the server", - "5xx": "Server error", - "5xx_text": "There was an internal server error (code: {code})" - }, - "role": { - "emptyclaim": "A permission needs both a key and a value", - "nosection": "You have to select at least one section to give users access to" - }, - "changepassword": { - "emptyfields": "Please fill out all fields", - "nouser": "User is invalid", - "newpasswordsnotmatching": "The new password does not match the confirmation" - }, - "preview": { - "notfound": "Not found", - "notfound_text": "Could not find entity with ID \"{id}\"" - }, - "page": { - "parentnotallowed": "The page is not allowed as a children of the selected parent", - "sortingmultipleparents": "Sorting pages is only allowed for a specific parent page" - }, - "forms": { - "email_invalid": "The email address is not valid", - "emails_invalid": "The email address(es) is/are not valid", - "hex_format": "The input is not in a valid HEX #aabbcc format", - "url_format": "The input is no well-formed URL", - "not_unique": "The property must be unique in this collection", - "not_unique_alone": "There must be at least one entity in this collection which fulfills this condition", - "culture": "Could not find a culture for the given ISO code", - "reference_notfound": "The reference could not be found" - } - }, - - "unsavedchanges": { - "title": "Du hast ungespeicherte Änderungen", - "text": "Bist du sicher, dass du deine Änderungen verwerfen möchtest?", - "close": "Änderungen verwerfen", - "confirm": "Auf Seite bleiben" - }, - - "deleteoverlay": { - "close": "Abbrechen", - "confirm": "Löschen", - "title": "Entität löschen", - "text": "Bist du sicher, dass du diese Entität löschen möchtest?", - "success": "Gelöscht", - "success_text": "Du hast diese Entität erfolgreich gelöscht", - "page_text": "Du bist im Begriff diese Seite inklusive aller Unterseiten zu löschen. Bist du sicher?", - "page_success_text": "Du hast diese Seite erfolgreich gelöscht" - }, - - "changepasswordoverlay": { - "title": "Passwort ändern", - "confirm": "Passwort ändern", - "fields": { - "currentPassword": "Aktuelles Passwort", - "newPassword": "Neues Passwort", - "confirmNewPassword": "Neues Passwort bestätigen" - } - }, - - "sections": { - "item": { - "dashboard": "Dashboard", - "spaces": "Räume", - "media": "Medien", - "pages": "Seiten", - "settings": "Einstellungen" - } - }, - - "settings": { - "groups": { - "system": "System", - "plugins": "Plugins", - "integrations": "Integrationen" - }, - "system": { - "updates": { - "name": "Updates", - "text": "Version {zero_version}", - "text_default": "Aktuelle zero Version aktualisieren" - }, - "applications": { - "name": "Applikationen", - "text": "Website Applikationen verwalten" - }, - "users": { - "name": "User & Berechtigungen", - "text": "Administration der Backend Benutzer" - }, - "languages": { - "name": "Sprachen", - "text": "Frontend Sprachen" - }, - "translations": { - "name": "Übersetzungen", - "text": "Frontend Texte und Übersetzungen" - }, - "countries": { - "name": "Länder", - "text": "Liste an Ländern verwalten" - }, - "mails": { - "name": "Mails", - "text": "Mailvorlagen bearbeiten" - }, - "logs": { - "name": "Logs", - "text": "Fehler diagnostizieren" - } - }, - "plugins": { - "installed": { - "name": "Installierte Plugins", - "text": "{plugin_count} installierte(s) Plugin(s)", - "text_default": "Verwalte und installiere Plugins" - }, - "create": { - "name": "Plugin erstellen", - "text": "Plugin anhand vorhandenem Code erstellen" - }, - "integrations": { - "name": "Integrationen", - "text": "Konfigurieren von vorhandenen Integrationen" - } - } - }, - - "user": { - "name": "Benutzer", - "users": "Benutzer", - "add_user": "Neuer Benutzer", - "add_role": "Neue Rolle", - "title": "User & Berechtigungen", - "count_permissions": "{count} Berechtigungen", - "one_permission": "1 Berechtigung", - "tab_permissions": "Berechtigungen", - "changePassword": "Passwort ändern", - "fields": { - "name_placeholder": "Gib deinen echten Namen oder Benutzernamen ein", - "email": "Email", - "email_text": "Wird auch als Username für den Login verwendet", - "email_placeholder": "Gib deine Email-Adresse ein", - "avatarId": "Bild", - "avatarId_text": "Lade ein Benutzerbild hoch", - "languageId": "Sprache", - "languageId_text": "Lege die Backend Sprache für diesen Benutzer fest", - "roles": "Benutzerrollen", - "roles_text": "Füge Berechtigungen anhand der Rollenzuweisung hinzu", - "sections": "Bereiche", - "sections_text": "Zugangsberechtigungen für Backend-Bereiche", - "isActive": "Kann sich anmelden", - "isDisabled": "Deaktivieren", - "isLockedOut": "Ist gesperrt", - "isLockedOut_warning": "Der Benutzer ist gesperrt bis", - "isDisabled_warning": "Dieser Benutzer ist deaktiviert und kann sich nicht mehr in zero einloggen. Der Status kann im Aktionen Menü geändert werden" - } - }, - - "role": { - "name": "Benutzerrollen", - "roles": "Benutzerrollen", - "tab_permissions": "Berechtigungen", - "fields": { - "description": "Beschreibung", - "icon": "Symbol" - } - }, - - "permission": { - "states": { - "create": "Erstellen", - "read": "Ansehen", - "update": "Bearbeiten", - "delete": "Löschen", - "write": "Bearbeiten", - "createdelete": "Erstellen/Löschen", - "none": "Keine" - }, - "collections": { - "sections": "Bereiche", - "settings": "Einstellungen", - "spaces": "Räume", - "modules": "Module" - } - }, - - "login": { - "headline": "hello and welcome", - "button": "Login", - "button_forgot": "Forgot password?", - "fields": { - "email": "Email", - "email_placeholder": "Enter your email or username", - "password": "Password", - "password_placeholder": "Enter your password" - }, - "errors": { - "wrongcredentials": "Email or password is wrong", - "lockedout": "The user has been locked out due to many failed password attempts", - "disabled": "The user has been deactivated", - "notallowed": "The user is not allowed to sign in", - "requirestwofactor": "Two-factor authentication is required to login" - }, - "rejectReasons": { - "logout": "You have successfully logged out", - "inactive": "You have been inactive for too long. Please login again.", - "terminated": "Your session has been terminated. Please login again.", - "userchanged": "Your user data has changed. Please login again.", - "passwordchanged": "Your password was successfully changed. Please login again." - } - }, - - "modules": { - "add": { - "title": "Modul hinzufügen", - "text": "Stelle die Seite zusammen, in dem du Module hinzufügst" - }, - "default_group": "Standard", - "notfound": "Ein module mit dem Alias [{alias}] konnte nicht gefunden werden." - }, - - "page": { - "name": "Seite", - "type": "Typ", - "root": "Oberste Ebene", - "info_tab": "Info", - - "folder": { - "name": "Ordner", - "description": "Seiten gruppieren", - "fields": { - "isPartOfRoute": "Als Teil der URL darstellen", - "isPartOfRoute_text": "Dieser Ordner wird als Teil der URL dargestellt", - "urlAlias": "URL Alias", - "urlAlias_text": "Dieses Feld wird für die Generierung der URL anstatt das Namens verwendet" - } - }, - - "overview": { - "actions": { - "continue": "Fortsetzen", - "continue_text": "Zuletzt wurde \"{page}\" bearbeitet", - "new": "Neue Seite", - "new_text": "Eine neue Seite in der obersten Ebene erstellen", - "history": "Verlauf", - "history_text": "Bearbeitungsverlauf von Seiten anzeigen" - } - }, - "schedule": { - "label": "Planen", - "header": "Veröffentlichung planen", - "publish": "Veröffentlichen", - "unpublish": "Veröffentlichung aufheben", - "scheduled": "Geplant" - }, - - "actions": { - "emptyrecyclebin": "Papierkorb leeren" - }, - - "create": { - "title": "Seite erstellen", - "parent": "Ebene oberhalb", - "nonavailable": "Hier sind keine Seiten als Unterseiten erlaubt" - }, - - "preview": { - "title": "Vorschau" - }, - - "picker": { - "headline": "Seite auswählen" - } - }, - - "recyclebin": { - "name": "Papierkorb", - "description": "Zuletzt gelöscht Elemente anzeigen", - "purge": "Alles löschen", - "fields": { - "originalId": "Seiten-ID", - "createdDate": "Gelöscht" - }, - "overlay": { - "title": "Aktionen" - } - }, - - "country": { - "list": "Länder", - "name": "Land", - "fields": { - "code": "Code", - "code_text": "Code im ISO 3166-1 Format, welcher zum Anzeigen von Flaggen benötigt wird", - "isPreferred": "Oben anzeigen" - } - }, - - "integration": { - "list": "Integrationen", - "name": "Integration", - "activeWarning": "Diese Integration ist deaktiviert und wird daher unter keinen Umständen laufen.", - "errors": { - "typenotfound": "Dieser Integrationstyp existiert leider nicht", - "multiplenotallowed": "Je Integrationstyp kann nur eine Integration angelegt werden", - "notfound": "Eine passende Integration konnte nicht gefunden werden", - "couldnotupdatestate": "Nicht aktualisiert" - }, - "fields": { - } - }, - - "language": { - "list": "Sprachen", - "name": "Sprache", - "fields": { - "name": "Name", - "locale": "Locale", - "code": "ISO Code", - "code_text": "Code im ISO 3166-1 Format", - "isDefault": "Primär", - "isDefault_text": "Diese Sprache als Standardsprache verwenden", - "isOptional": "Optional", - "isOptional_text": "Werte müssen bei optionalen Sprachen nicht eingegeben werden, selbst wenn das Feld erforderlich ist", - "inheritedLanguageId": "Ersatzsprache", - "inheritedLanguageId_text": "Bei fehlendem Inhalt kann Content aus einer Ersatzsprache verwendet werden" - }, - "errors": { - "fallback_invalid": "Verwende eine andere Sprache als Ersatzsprache", - "default_unique": "Es gibt bereits eine Sprache die als Primärsprache verwendet wird", - "default_not_optional": "Die Primärsprache kann nicht optional sein", - "default_no_fallback": "Die Primärsprache kann keine Ersatzsprache haben", - "needs_default": "Zumindest eine Sprache muss als Primärsprache festgelegt werden" - } - }, - - "translation": { - "list": "Übersetzungen", - "name": "Übersetzung", - "display": { - "text": "Text", - "html": "HTML" - }, - "fields": { - "key": "Schlüssel", - "value": "Wert", - "display": "Anzeigetyp" - } - }, - - "iconpicker": { - "title": "Symbol auswählen" - }, - - "colorpicker": { - "placeholder": "Wähle eine Farbe aus (#aabbcc)" - }, - - "mediapicker": { - "select_text": "Medien hinzufügen", - "select_description": "Hochladen oder auswählen", - "upload_text": "Hochladen", - "upload_description": "Ein neue Datei hochladen" - }, - - "videopicker": { - "headline": "Video auswählen", - "providers": { - "html": "Datei hochladen", - "youtube": "YouTube", - "vimeo": "Vimeo" - } - }, - - "application": { - "list": "Applikationen", - "name": "Applikation", - "tab_features": "Features", - "tab_domains": "Domains", - "fields": { - "name": "Name", - "name_text": "Kurzer leserlicher Name", - "fullName": "Vollständiger Name", - "fullName_text": "Vollständiger Firmen- oder Produktname", - "imageid": "Bild", - "imageid_text": "Bild der Applikation (mit transparentem Hintegrund), wird für alle Mails, PDFs, ... benützt", - "iconid": "Symbol", - "iconid_text": "Vereinfachtes Bild als Symbol für kleinere Anwendungen (z.B. Favicon)", - "email": "Email", - "email_text": "Kontakt-Email-Adresse", - "domains": "Domains", - "domains_text": "Wähle Domains für diese Applikation aus", - "domains_help": "Gültige Domainnamen sind: \"example.com\", \"www.example.com\", \"example.com:8080\", or \"https://www.example.com\".", - "domains_add": "Domain hinzufügen", - "features": "Features", - "features_text": "Hier kannst du zusätzliche Features für deine Applikation freischalten" - } - }, - - "media": { - "list": "Medien", - "name": "Medien", - "addfolder": "Ordner erstellen", - "editfolder": "Ordner bearbeiten", - "actions": { - "folderdropdown": "Ordner", - "addfolder": "Ordner erstellen", - "addfolder_text": "Fügt dem aktuellen Ordner einen neuen Unterordner hinzu", - "addfile": "Dateien hochladen", - "addfile_text": "Eine oder mehrere Dateien dem aktuellen Ordner hinzufügen" - }, - "selection": { - "clear": "Auswahl aufheben" - }, - "fields": { - "foldername_placeholder": "Name eingeben ...", - "caption": "Unterschrift", - "caption_text": "Zusätzlicher Beschreibungstext", - "dimension": "Dimensionen", - "size": "Dateigröße", - "date": "Zuletzt geändert", - "filename": "Dateiname", - "source": "Datei", - "alternativeText": "Alternativtext", - "alternativeText_text": "Dieser Text wird dargestellt, wenn das Bild nicht geladen werden kann", - "dpi": "DPI", - "colorSpace": "Farbraum", - "hasTransparency": "Beinhaltet Transparenz", - "frames": "Bilder" - }, - "deleteoverlay": { - "folder_text": "Diese Aktion löscht den Ordner sowie alle Unterordner und Dateien. Bist du dir sicher?", - "folder_success_text": "Du hast den Ordner erfolgreich gelöscht" - }, - "child_count_1": "{count} Element", - "child_count_x": "{count} Elemente" - }, - - "mailTemplate": { - "name": "Mailvorlagen", - "list": "Mailvorlage", - "emailBox": "Email Einstellungen", - "tabs": { - "sender": "Absender", - "recipient": "Empfänger" - }, - "fields": { - "key": "Schlüssel", - "key_text": "Wird benötigt, um die Vorlage im Code abrufen zu können", - "subject": "Betreff", - "body": "Inhalt", - "body_text": "Zusätzlicher Inhaltstext, welcher in der Mail dargestellt wird", - "preheader": "Preheader", - "preheader_text": "Dieser Text wird in der Vorschau eines Mailprogramms dargestellt", - "senderEmail": "Absender-Email", - "senderEmail_text": "Leer lassen, um Applikations-Email zu verwenden", - "senderName": "Absendername", - "senderName_text": "Leer lassen, um Applikationsname zu verwenden", - "recipientEmail": "Empfänger-Email", - "recipientEmail_text": "Wird nur für Mails benützt, die keinen dynamischen Empfänger haben (z.B. Reporte)", - "cc": "Kopie senden an (CC)", - "cc_text": "Mehrere Emails mit Komma trennen", - "bcc": "Unsichtbare Kopie senden an (BCC)", - "bcc_text": "Mehrere Emails mit Komma trennen" - }, - "errors": { - "senderNotAllowed": "Die Absender-Email ist für den aktuellen Mail-Service nicht freigeschalten. Bitte kontaktiere den Administrator für Hilfe." - } - }, - - "space": { - "list": "Räume", - "name": "Raum" - }, - - "preview": { - "name": "Vorschau" - }, - - "search": { - "collection": { - "page": "Seite", - "mediaFolder": "Medienordner" - } - } -} \ No newline at end of file diff --git a/old/zero.Web/Security/AuthenticationBuilderExtensions.cs b/old/zero.Web/Security/AuthenticationBuilderExtensions.cs deleted file mode 100644 index 174ab82e..00000000 --- a/old/zero.Web/Security/AuthenticationBuilderExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System; -using System.Threading.Tasks; -using zero.Core; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Options; -using zero.Core.Security; - -namespace zero.Web.Security -{ - public static class AuthenticationBuilderExtensions - { - public static AuthenticationBuilder AddZeroBackofficeCookie(this AuthenticationBuilder builder, Action> setupAction = null) - where TUser : BackofficeUser - where TRole : BackofficeUserRole - { - return builder.AddCookie(Constants.Auth.BackofficeScheme, Constants.Auth.BackofficeDisplayName, true, b => - { - b.Configure((options, zero) => - { - options.ExpireTimeSpan = TimeSpan.FromMinutes(90); - options.Cookie.SameSite = SameSiteMode.Lax; - options.Cookie.Name = Constants.Auth.BackofficeCookieName; - - options.CookieManager = new ContextualCookieManager((ctx, key) => - { - return ctx.Request.Path.ToString().StartsWith(zero.BackofficePath.EnsureStartsWith('/').TrimEnd('/')); - }); - - options.Events.OnRedirectToLogin = ctx => - { - ctx.Response.StatusCode = 401; - return Task.CompletedTask; - }; - }); - - if (setupAction != null) - { - setupAction(b); - } - }); - } - - - - public static AuthenticationBuilder AddZeroCookie(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action> setupAction = null) - where TUser : ZeroIdentityUser - where TRole : ZeroIdentityRole - { - return builder.AddCookie(scheme, cookieDisplayName, false, setupAction); - } - - - - public static AuthenticationBuilder AddZeroCookie(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action> setupAction = null) - where TUser : ZeroIdentityUser - { - return builder.AddCookie(scheme, cookieDisplayName, false, setupAction); - } - - - - static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string scheme, string displayName, bool isBackoffice, Action> setupAction = null) - where TUser : ZeroIdentityUser - { - IServiceCollection services = builder.Services; - - services - .AddOptions>() - .Configure((options, zero) => - { - options.Scheme = scheme; - options.Path = isBackoffice ? zero.BackofficePath : "/"; - }); - - var optionsBuilder = services - .AddOptions(scheme) - .Configure>>((options, monitor) => - { - ZeroAuthOptions opts = monitor.Get(scheme); - - options.SlidingExpiration = true; - options.ExpireTimeSpan = TimeSpan.FromDays(90); - options.Cookie.HttpOnly = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; - options.Cookie.SameSite = SameSiteMode.Lax; - options.Cookie.Path = "/"; - - options.LoginPath = opts.Path; - options.LogoutPath = opts.Path; - options.AccessDeniedPath = opts.Path; - }); - - if (setupAction != null) - { - setupAction(optionsBuilder); - } - - builder.AddScheme(scheme, displayName, null); - - return builder; - } - } -} \ No newline at end of file diff --git a/old/zero.Web/ZeroClaimsPrinicipalFactory.cs b/old/zero.Web/ZeroClaimsPrinicipalFactory.cs deleted file mode 100644 index b7230333..00000000 --- a/old/zero.Web/ZeroClaimsPrinicipalFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -//using Microsoft.AspNetCore.Identity; -//using Microsoft.Extensions.Options; -//using System.Collections.Generic; -//using System.Linq; -//using System.Security.Claims; -//using System.Threading.Tasks; -//using zero.Core; -//using zero.Core.Entities; -//using zero.Core.Identity; - -//namespace zero.Web -//{ -// public class ZeroClaimsPrinicipalFactory : UserClaimsPrincipalFactory -// { -// public ZeroClaimsPrinicipalFactory(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) : base(userManager, roleManager, optionsAccessor) -// { - -// } - -// public async override Task CreateAsync(User user) -// { -// ClaimsPrincipal principal = await base.CreateAsync(user); -// ClaimsIdentity identity = (ClaimsIdentity)principal.Identity; - -// identity.AddClaim(new Claim(Constants.Auth.Claims.IsZero, PermissionsValue.True)); - -// if (user.IsSuper) -// { -// identity.AddClaim(new Claim(Constants.Auth.Claims.IsSuper, PermissionsValue.True)); -// } - -// // get all allowed app ids -// IEnumerable permissions = identity.Claims -// .Where(claim => claim.Type == Constants.Auth.Claims.Permission && claim.Value.StartsWith(Permissions.Applications)) -// .Select(x => Permission.FromClaim(x, Permissions.Applications)); - -// string[] appIds = permissions.Where(x => x.IsTrue).Select(x => x.NormalizedKey).ToArray(); - -// string currentAppId = user.CurrentAppId ?? user.AppId; - -// if (!user.IsSuper && !appIds.Contains(currentAppId)) -// { -// currentAppId = user.AppId; -// } - -// identity.AddClaim(new Claim(Constants.Auth.Claims.CurrentAppId, currentAppId)); - -// return principal; -// } -// } -//} diff --git a/old/zero.Web/ZeroEndpointRouteBuilder.cs b/old/zero.Web/ZeroEndpointRouteBuilder.cs deleted file mode 100644 index 82dbc580..00000000 --- a/old/zero.Web/ZeroEndpointRouteBuilder.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using System; -using System.Collections.Generic; - -namespace zero.Web -{ - public class ZeroEndpointRouteBuilder : IZeroEndpointRouteBuilder - { - IEndpointRouteBuilder _builder; - - public IServiceProvider ServiceProvider => _builder.ServiceProvider; - - public ICollection DataSources => _builder.DataSources; - - internal ZeroEndpointRouteBuilder(IEndpointRouteBuilder builder) - { - _builder = builder; - } - - public IApplicationBuilder CreateApplicationBuilder() => _builder.CreateApplicationBuilder(); - } - - - public interface IZeroEndpointRouteBuilder : IEndpointRouteBuilder - { - - } -} diff --git a/old/zero.Web/ZeroEnpointRouteBuilderExtensions.cs b/old/zero.Web/ZeroEnpointRouteBuilderExtensions.cs deleted file mode 100644 index 13c09a0d..00000000 --- a/old/zero.Web/ZeroEnpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using zero.Core.Routing; - -namespace zero.Web -{ - public static class ZeroEndpointRouteBuilderExtensions - { - public static IZeroEndpointRouteBuilder MapZeroBackoffice(this IZeroEndpointRouteBuilder endpoints, string path = "/zero") - { - //IZeroOptions options = app.ApplicationServices.GetService(); // TODO oO - // see https://our.umbraco.com/documentation/reference/routing/custom-routes#where-to-put-your-routing-logic - //string path = options.BackofficePath.EnsureStartsWith('/').TrimEnd('/'); - - endpoints.MapFallbackToController(path + "/{**path}", "Index", "ZeroBackoffice"); - return endpoints; - } - - - public static IZeroEndpointRouteBuilder MapZeroRoutes(this IZeroEndpointRouteBuilder endpoints) - { - endpoints.MapDynamicControllerRoute("/{**url}", state: null, order: 10); - return endpoints; - } - } -} diff --git a/old/zero.Core/Options/BackofficeJsonSerlializerSettings.cs b/zero.Backoffice/BackofficeJsonSerlializerSettings.cs similarity index 100% rename from old/zero.Core/Options/BackofficeJsonSerlializerSettings.cs rename to zero.Backoffice/BackofficeJsonSerlializerSettings.cs diff --git a/zero.Backoffice/DevServer/EventedStreamReader.cs b/zero.Backoffice/DevServer/EventedStreamReader.cs new file mode 100644 index 00000000..55bdd8a5 --- /dev/null +++ b/zero.Backoffice/DevServer/EventedStreamReader.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation Contributors. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Original Source: https://github.com/aspnet/JavaScriptServices + +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace zero.Backoffice.DevServer; + +/// +/// Wraps a to expose an evented API, issuing notifications +/// when the stream emits partial lines, completed lines, or finally closes. +/// +internal class EventedStreamReader +{ + public delegate void OnReceivedChunkHandler(ArraySegment chunk); + public delegate void OnReceivedLineHandler(string line); + public delegate void OnStreamClosedHandler(); + + public event OnReceivedChunkHandler OnReceivedChunk; + public event OnReceivedLineHandler OnReceivedLine; + public event OnStreamClosedHandler OnStreamClosed; + + private readonly StreamReader _streamReader; + private readonly StringBuilder _linesBuffer; + + public EventedStreamReader(StreamReader streamReader) + { + _streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader)); + _linesBuffer = new StringBuilder(); + Task.Factory.StartNew(Run); + } + + public Task WaitForMatch(Regex regex) + { + var tcs = new TaskCompletionSource(); + var completionLock = new object(); + + OnReceivedLineHandler onReceivedLineHandler = null; + OnStreamClosedHandler onStreamClosedHandler = null; + + void ResolveIfStillPending(Action applyResolution) + { + lock (completionLock) + { + if (!tcs.Task.IsCompleted) + { + OnReceivedLine -= onReceivedLineHandler; + OnStreamClosed -= onStreamClosedHandler; + applyResolution(); + } + } + } + + onReceivedLineHandler = line => + { + var match = regex.Match(line); + if (match.Success) + { + ResolveIfStillPending(() => tcs.SetResult(match)); + } + }; + + onStreamClosedHandler = () => + { + ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException())); + }; + + OnReceivedLine += onReceivedLineHandler; + OnStreamClosed += onStreamClosedHandler; + + return tcs.Task; + } + + + public Task WaitForFinish() + { + var tcs = new TaskCompletionSource(); + var completionLock = new object(); + + OnStreamClosedHandler onStreamClosedHandler = null; + + void ResolveIfStillPending(Action applyResolution) + { + lock (completionLock) + { + if (!tcs.Task.IsCompleted) + { + OnStreamClosed -= onStreamClosedHandler; + applyResolution(); + } + } + } + + onStreamClosedHandler = () => + { + ResolveIfStillPending(() => tcs.SetResult()); + }; + + OnStreamClosed += onStreamClosedHandler; + + return tcs.Task; + } + + + private async Task Run() + { + var buf = new char[8 * 1024]; + while (true) + { + var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length); + if (chunkLength == 0) + { + OnClosed(); + break; + } + + OnChunk(new ArraySegment(buf, 0, chunkLength)); + + int lineBreakPos = -1; + int startPos = 0; + + // get all the newlines + while ((lineBreakPos = Array.IndexOf(buf, '\n', startPos, chunkLength - startPos)) >= 0 && startPos < chunkLength) + { + var length = lineBreakPos - startPos; + _linesBuffer.Append(buf, startPos, length); + OnCompleteLine(_linesBuffer.ToString()); + _linesBuffer.Clear(); + startPos = lineBreakPos + 1; + } + + // get the rest + if (lineBreakPos < 0 && startPos < chunkLength) + { + _linesBuffer.Append(buf, startPos, chunkLength - startPos); + } + } + } + + private void OnChunk(ArraySegment chunk) + { + var dlg = OnReceivedChunk; + dlg?.Invoke(chunk); + } + + private void OnCompleteLine(string line) + { + var dlg = OnReceivedLine; + dlg?.Invoke(line); + } + + private void OnClosed() + { + var dlg = OnStreamClosed; + dlg?.Invoke(); + } +} + +/// +/// Captures the completed-line notifications from a , +/// combining the data into a single . +/// +internal class EventedStreamStringReader : IDisposable +{ + private EventedStreamReader _eventedStreamReader; + private bool _isDisposed; + private StringBuilder _stringBuilder = new StringBuilder(); + + public EventedStreamStringReader(EventedStreamReader eventedStreamReader) + { + _eventedStreamReader = eventedStreamReader + ?? throw new ArgumentNullException(nameof(eventedStreamReader)); + _eventedStreamReader.OnReceivedLine += OnReceivedLine; + } + + public string ReadAsString() => _stringBuilder.ToString(); + + private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line); + + public void Dispose() + { + if (!_isDisposed) + { + _eventedStreamReader.OnReceivedLine -= OnReceivedLine; + _isDisposed = true; + } + } +} diff --git a/zero.Backoffice/DevServer/PidUtils.cs b/zero.Backoffice/DevServer/PidUtils.cs new file mode 100644 index 00000000..63db6caa --- /dev/null +++ b/zero.Backoffice/DevServer/PidUtils.cs @@ -0,0 +1,201 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace zero.Backoffice.DevServer; + +public static class PidUtils +{ + const string ssPidRegex = @"(?:^|"",|"",pid=)(\d+)"; + const string portRegex = @"[^]*[.:](\\d+)$"; + + public static int GetPortPid(ushort port) + { + int pidOut = -1; + + int portColumn = 1; // windows + int pidColumn = 4; // windows + string pidRegex = null; + + List results = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + results = RunProcessReturnOutputSplit("netstat", "-anv -p tcp"); + results.AddRange(RunProcessReturnOutputSplit("netstat", "-anv -p udp")); + portColumn = 3; + pidColumn = 8; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + results = RunProcessReturnOutputSplit("ss", "-tunlp"); + portColumn = 4; + pidColumn = 6; + pidRegex = ssPidRegex; + } + else + { + results = RunProcessReturnOutputSplit("netstat", "-ano"); + } + + + foreach (var line in results) + { + if (line.Length <= portColumn || line.Length <= pidColumn) continue; + try + { + // split lines to words + var portMatch = Regex.Match(line[portColumn], $"[.:]({port})"); + if (portMatch.Success) + { + int portValue = int.Parse(portMatch.Groups[1].Value); + + if (pidRegex == null) + { + pidOut = int.Parse(line[pidColumn]); + return pidOut; + } + else + { + var pidMatch = Regex.Match(line[pidColumn], pidRegex); + if (pidMatch.Success) + { + pidOut = int.Parse(pidMatch.Groups[1].Value); + } + } + } + } + catch (Exception) + { + // ignore line error + } + } + + return pidOut; + } + + private static List RunProcessReturnOutputSplit(string fileName, string arguments) + { + string result = RunProcessReturnOutput(fileName, arguments); + if (result == null) return new List(); + + string[] lines = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var lineWords = new List(); + foreach (var line in lines) + { + lineWords.Add(line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + } + return lineWords; + } + + private static string RunProcessReturnOutput(string fileName, string arguments) + { + Process process = null; + try + { + var si = new ProcessStartInfo(fileName, arguments) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + process = Process.Start(si); + var stdOutT = process.StandardOutput.ReadToEndAsync(); + var stdErrorT = process.StandardError.ReadToEndAsync(); + if (!process.WaitForExit(10000)) + { + try { process?.Kill(); } catch { } + } + + if (Task.WaitAll(new Task[] { stdOutT, stdErrorT }, 10000)) + { + // if success, return data + return (stdOutT.Result + Environment.NewLine + stdErrorT.Result).Trim(); + } + return null; + } + catch (Exception) + { + return null; + } + finally + { + process?.Close(); + } + } + + public static bool Kill(string process, bool ignoreCase = true, bool force = false, bool tree = true) + { + var args = new List(); + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (force) { args.Add("-9"); } + if (ignoreCase) { args.Add("-i"); } + args.Add(process); + RunProcessReturnOutput("pkill", string.Join(" ", args)); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (force) { args.Add("-9"); } + if (ignoreCase) { args.Add("-I"); } + args.Add(process); + RunProcessReturnOutput("killall", string.Join(" ", args)); + } + else + { + if (force) { args.Add("/f"); } + if (tree) { args.Add("/T"); } + args.Add("/im"); + args.Add(process); + return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false; + } + return true; + } + catch (Exception) + { + + } + return false; + } + + public static bool Kill(int pid, bool force = false, bool tree = true) + { + if (pid == -1) { return false; } + + var args = new List(); + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (force) { args.Add("-9"); } + args.Add(pid.ToString()); + RunProcessReturnOutput("kill", string.Join(" ", args)); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (force) { args.Add("-9"); } + args.Add(pid.ToString()); + RunProcessReturnOutput("kill", string.Join(" ", args)); + } + else + { + if (force) { args.Add("/f"); } + if (tree) { args.Add("/T"); } + args.Add("/PID"); + args.Add(pid.ToString()); + return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false; + } + return true; + } + catch (Exception) + { + } + return false; + } + + public static bool KillPort(ushort port, bool force = false, bool tree = true) => Kill(GetPortPid(port), force: force, tree: tree); + +} diff --git a/zero.Backoffice/DevServer/ProcessProxy.cs b/zero.Backoffice/DevServer/ProcessProxy.cs new file mode 100644 index 00000000..83b2d63b --- /dev/null +++ b/zero.Backoffice/DevServer/ProcessProxy.cs @@ -0,0 +1,248 @@ +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace zero.Backoffice.DevServer; + +public class ProcessProxy +{ + string workingDirectory = null; + string script = null; + Action onProcessConfigure = null; + Action captureLog = null; + bool isWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + Dictionary envVars = new(); + HashSet arguments = new(); + bool forwardLog = false; + + Process process = null; + EventedStreamReader stdOut = null; + EventedStreamReader stdErr = null; + + + + + public ProcessProxy(string workingDirectory, string script, bool forwardLog = false) + { + this.workingDirectory = workingDirectory; + this.script = script; + this.forwardLog = forwardLog; + } + + + /// + /// Adds an environment variable + /// + public ProcessProxy EnvVar(string key, string value) + { + envVars.Add(key, value); + return this; + } + + + /// + /// Adds an argument + /// + public ProcessProxy Argument(string argument) + { + arguments.Add(argument); + return this; + } + + + /// + /// Configures the process start info + /// + public ProcessProxy Configure(Action onProcessConfigure) + { + this.onProcessConfigure = onProcessConfigure; + return this; + } + + + /// + /// Capture the log instead of outputting it to the console + /// + public ProcessProxy Capture(Action action) + { + this.captureLog = action; + return this; + } + + + /// + /// Run the script and wait for completion. + /// This is only recommended for scripts which finish automatically and have no user interaction. + /// + public async Task ExecuteAsync(TimeSpan timeout = default) + { + StartProcess(); + + using var stdErrReader = new EventedStreamStringReader(stdErr); + try + { + // TODO implement timeout + // https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource + await stdOut.WaitForFinish(); + } + catch (EndOfStreamException ex) + { + throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); + } + } + + + /// + /// Run the script and return as soon as a condition is met. + /// The script will not cancel and will continue to run as a sub-process as long as it does not auto-close. + /// + public async Task RunAsync(string startupCondition, TimeSpan startupTimeout = default) + { + StartProcess(); + + using var stdErrReader = new EventedStreamStringReader(stdErr); + try + { + await stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout)); + } + catch (EndOfStreamException ex) + { + throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex); + } + } + + + public void Exit() + { + try { process?.Kill(); } catch { } + try { process?.WaitForExit(); } catch { } + + AppDomain.CurrentDomain.DomainUnload -= UnloadHandler; + AppDomain.CurrentDomain.ProcessExit -= UnloadHandler; + AppDomain.CurrentDomain.UnhandledException -= UnloadHandler; + } + + + /// + /// Run + /// + Process StartProcess() + { + string executable = script; + + StringBuilder command = new(); + command.Append(script); + command.Append(' '); + foreach (string arg in arguments) + { + command.Append(arg); + } + string argumentList = command.ToString(); + + if (isWindows) + { + argumentList = $"/c {argumentList}"; + executable = "cmd"; + } + + ProcessStartInfo startInfo = new(executable) + { + Arguments = argumentList, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = workingDirectory + }; + + foreach (var envVar in envVars) + { + startInfo.Environment[envVar.Key] = envVar.Value; + } + + onProcessConfigure?.Invoke(startInfo); + + try + { + process = Process.Start(startInfo); + process.EnableRaisingEvents = true; + } + catch (Exception ex) + { + var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n" + + $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n" + + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n" + + " Make sure the executable is in one of those directories, or update your PATH.\n\n" + + "[2] See the InnerException for further details of the cause."; + throw new InvalidOperationException(message, ex); + } + + AttachLogger(); + + AppDomain.CurrentDomain.DomainUnload += UnloadHandler; + AppDomain.CurrentDomain.ProcessExit += UnloadHandler; + AppDomain.CurrentDomain.UnhandledException += UnloadHandler; + + return process; + } + + + void UnloadHandler(object sender, EventArgs e) + { + Exit(); + } + + + void AttachLogger(bool isStream = false) + { + stdOut = new EventedStreamReader(process.StandardOutput); + stdErr = new EventedStreamReader(process.StandardError); + + stdOut.OnReceivedLine += line => WriteToLog(line); + stdOut.OnReceivedChunk += chunk => WriteToLog(chunk); + stdErr.OnReceivedLine += line => WriteToLog(line, true); + stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true); + } + + + void WriteToLog(string line, bool isError = false) + { + if (String.IsNullOrWhiteSpace(line)) + { + return; + } + + line = line.StartsWith("") ? line.Substring(3) : line; + + if (captureLog != null) + { + captureLog(line, isError); + } + if (forwardLog) + { + (isError ? Console.Error : Console.Out).WriteLine(line); + } + } + + + void WriteToLog(ArraySegment chunk, bool isError = false) + { + bool containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0; + + if (containsNewline) + { + return; + } + + if (captureLog != null) + { + captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError); + } + if (forwardLog) + { + (isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count); + } + } +} diff --git a/zero.Backoffice/DevServer/ZeroDevOptions.cs b/zero.Backoffice/DevServer/ZeroDevOptions.cs new file mode 100644 index 00000000..79a55436 --- /dev/null +++ b/zero.Backoffice/DevServer/ZeroDevOptions.cs @@ -0,0 +1,12 @@ +namespace zero.Backoffice.DevServer; + +public class ZeroDevOptions +{ + public int Port { get; set; } = 3399; + + public bool ForwardLog { get; set; } = false; + + public string WorkingDirectory { get; set; } + + public bool Enabled { get; set; } = true; +} diff --git a/zero.Backoffice/DevServer/ZeroDevService.cs b/zero.Backoffice/DevServer/ZeroDevService.cs new file mode 100644 index 00000000..45d4cf50 --- /dev/null +++ b/zero.Backoffice/DevServer/ZeroDevService.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace zero.Backoffice.DevServer; + +public class ZeroDevService : IHostedService +{ + IWebHostEnvironment env; + IOptions options; + ProcessProxy viteProcess; + ILogger logger; + IEnumerable plugins; + string workingDirectory; + bool isRunning = false; + + + public ZeroDevService(IWebHostEnvironment env, IOptions options, ILogger logger, IEnumerable plugins) + { + //foreach (IZeroPlugin plugin in plugins) + //{ + // string location = Assembly.GetAssembly(plugin.GetType()). ; + //} + this.plugins = plugins; + this.env = env; + this.options = options; + this.workingDirectory = options.Value.WorkingDirectory; + this.logger = logger; + } + + + public async Task StartAsync(CancellationToken cancellationToken) + { + // this is a development-time service, + // therefore no way to enable it in production + if (!env.IsDevelopment() || !options.Value.Enabled) + { + return; + } + + // locate npm version and throw if it is not installed + Version npmVersion = await FindNpmVersion(); + + if (npmVersion == null) + { + throw new Exception("Please install node+npm to use the zero dev service (https://www.npmjs.com/)"); + } + + // start vite server + viteProcess = await StartDevServer(options.Value.Port); + logger.LogInformation("vite listening on: http://localhost:{port}", options.Value.Port); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + + /// + /// Get the node version from the system + /// + async Task FindNpmVersion() + { + Version version = null; + + ProcessProxy process = new ProcessProxy(workingDirectory, "npm").Argument("-v").Capture((value, err) => + { + if (version == null && !value.Contains("not recognized") && Version.TryParse(value, out Version _version)) + { + version = _version; + } + }); + + await process.ExecuteAsync(); + + return version; + } + + + /// + /// Starts the vite dev server which also support HMR + /// + async Task StartDevServer(int port) + { + // if the port we want to use is occupied, terminate the process utilizing that port. + // this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process + PidUtils.KillPort((ushort)port, true); + + // get all plugins which need to be passed to vite + List plugins = new(); + + foreach (var plugin in this.plugins) + { + if (!String.IsNullOrEmpty(plugin.Options.PluginPath)) + { + plugins.Add(plugin.Options.PluginPath); + } + } + + // create and run the vite script + ProcessProxy process = new ProcessProxy(workingDirectory, "npm", options.Value.ForwardLog) + .Argument("run dev") + .EnvVar("PORT", port.ToString()) + .EnvVar("ZERO_PLUGINS", JsonConvert.SerializeObject(plugins)) + .Capture(CaptureLog); + + await process.RunAsync("localhost:", TimeSpan.FromMinutes(1)); + + isRunning = true; + + return process; + } + + + void CaptureLog(string line, bool isError) + { + if (!isRunning) + { + return; + } + + if (isError) + { + logger.LogWarning(line); + } + else + { + logger.LogInformation(line); + } + } +} diff --git a/zero.Backoffice/Abstractions/BackofficeController.cs b/zero.Backoffice/Endpoints/BackofficeController.cs similarity index 100% rename from zero.Backoffice/Abstractions/BackofficeController.cs rename to zero.Backoffice/Endpoints/BackofficeController.cs diff --git a/zero.Backoffice/Endpoints/Countries.cs b/zero.Backoffice/Endpoints/Countries.cs deleted file mode 100644 index 962d0d3d..00000000 --- a/zero.Backoffice/Endpoints/Countries.cs +++ /dev/null @@ -1,23 +0,0 @@ -//using Microsoft.AspNetCore.Mvc; -//using Raven.Client.Documents; -//using Raven.Client.Documents.Linq; -//using System.Threading.Tasks; -//using zero; - -//namespace zero.Backoffice -//{ -// [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)] -// public class CountriesController : ZeroBackofficeCollectionController -// { -// public CountriesController(ICountriesCollection collection) : base(collection) -// { -// PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant(); -// } - -// public override Task> GetByQuery([FromQuery] ListBackofficeQuery query) -// { -// query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); -// return Collection.Load(query); -// } -// } -//} diff --git a/zero.Backoffice/Abstractions/ZeroBackofficeCollectionController.cs b/zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs similarity index 100% rename from zero.Backoffice/Abstractions/ZeroBackofficeCollectionController.cs rename to zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs diff --git a/zero.Backoffice/ZeroBackofficeControllerModelConvention.cs b/zero.Backoffice/Endpoints/ZeroBackofficeControllerModelConvention.cs similarity index 100% rename from zero.Backoffice/ZeroBackofficeControllerModelConvention.cs rename to zero.Backoffice/Endpoints/ZeroBackofficeControllerModelConvention.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_Languages.cs b/zero.Backoffice/Indexes/zero_Languages.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice/zero_Languages.cs rename to zero.Backoffice/Indexes/zero_Languages.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_MailTemplates.cs b/zero.Backoffice/Indexes/zero_MailTemplates.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice/zero_MailTemplates.cs rename to zero.Backoffice/Indexes/zero_MailTemplates.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_RecycledEntities.cs b/zero.Backoffice/Indexes/zero_RecycledEntities.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice/zero_RecycledEntities.cs rename to zero.Backoffice/Indexes/zero_RecycledEntities.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_Spaces.cs b/zero.Backoffice/Indexes/zero_Spaces.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice/zero_Spaces.cs rename to zero.Backoffice/Indexes/zero_Spaces.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice/zero_Translations.cs b/zero.Backoffice/Indexes/zero_Translations.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice/zero_Translations.cs rename to zero.Backoffice/Indexes/zero_Translations.cs diff --git a/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs b/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs new file mode 100644 index 00000000..74c1ec42 --- /dev/null +++ b/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using System.Threading.Tasks; +using zero; + +namespace zero.Backoffice.Modules; + +[ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)] +public class CountriesController : ZeroBackofficeCollectionController +{ + public CountriesController(ICountriesCollection collection) : base(collection) + { + PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant(); + } + + public override Task> GetByQuery([FromQuery] ListBackofficeQuery query) + { + query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); + return Collection.Load(query); + } +} \ No newline at end of file diff --git a/old/zero.Web/Models/CountryEditModel.cs b/zero.Backoffice/Modules/Countries/CountryEditModel.cs similarity index 100% rename from old/zero.Web/Models/CountryEditModel.cs rename to zero.Backoffice/Modules/Countries/CountryEditModel.cs diff --git a/old/zero.Web/Models/CountryListModel.cs b/zero.Backoffice/Modules/Countries/CountryListModel.cs similarity index 100% rename from old/zero.Web/Models/CountryListModel.cs rename to zero.Backoffice/Modules/Countries/CountryListModel.cs diff --git a/zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs b/zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs new file mode 100644 index 00000000..cba6d0f9 --- /dev/null +++ b/zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs @@ -0,0 +1,17 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Backoffice.Modules; + +public class Countries_Listing : ZeroIndex +{ + protected override void Create() + { + Map = items => items.Select(item => new + { + Name = item.Name, + IsPreferred = item.IsPreferred + }); + + Index(x => x.Name, FieldIndexing.Search); + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Countries/Module.cs b/zero.Backoffice/Modules/Countries/Module.cs new file mode 100644 index 00000000..c1c18852 --- /dev/null +++ b/zero.Backoffice/Modules/Countries/Module.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Backoffice.Modules; + +internal class CountriesModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + + } + + + /// + public override void Configure(IZeroOptions options) + { + options.Raven.Indexes.Add(); + } +} \ No newline at end of file diff --git a/old/zero.Core/Services/BackofficeSearchService.cs b/zero.Backoffice/Modules/Search/BackofficeSearchService.cs similarity index 100% rename from old/zero.Core/Services/BackofficeSearchService.cs rename to zero.Backoffice/Modules/Search/BackofficeSearchService.cs diff --git a/old/zero.Core/Database/Indexes/Backoffice_Search.cs b/zero.Backoffice/Modules/Search/Backoffice_Search.cs similarity index 100% rename from old/zero.Core/Database/Indexes/Backoffice_Search.cs rename to zero.Backoffice/Modules/Search/Backoffice_Search.cs diff --git a/old/zero.Web/Resources/Countries/countries.de-de.json b/zero.Backoffice/Resources/Countries/countries.de-de.json similarity index 100% rename from old/zero.Web/Resources/Countries/countries.de-de.json rename to zero.Backoffice/Resources/Countries/countries.de-de.json diff --git a/old/zero.Web/Resources/Countries/countries.en-us.json b/zero.Backoffice/Resources/Countries/countries.en-us.json similarity index 100% rename from old/zero.Web/Resources/Countries/countries.en-us.json rename to zero.Backoffice/Resources/Countries/countries.en-us.json diff --git a/old/zero.Web/Resources/Localization/zero.en-us.json b/zero.Backoffice/Resources/Localization/zero.en-us.json similarity index 100% rename from old/zero.Web/Resources/Localization/zero.en-us.json rename to zero.Backoffice/Resources/Localization/zero.en-us.json diff --git a/zero.Backoffice/Usings.cs b/zero.Backoffice/Usings.cs new file mode 100644 index 00000000..8edf5a97 --- /dev/null +++ b/zero.Backoffice/Usings.cs @@ -0,0 +1,25 @@ + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; + +global using zero.Architecture; +global using zero.Configuration; +global using zero.Extensions; +global using zero.Identity; +global using zero.Pages; +global using zero.Validation; +global using zero.Utils; +global using zero.Persistence; +global using zero.Localization; +global using zero.Routing; +global using zero.Context; +global using zero.Communication; +global using zero.Rendering; +global using zero.Media; +global using zero.Applications; + +global using zero.Backoffice; \ No newline at end of file diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index 085120f1..51337038 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -8,6 +8,30 @@ true + + + + + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + diff --git a/zero.Core/Application/Application.cs b/zero.Core/Applications/Application.cs similarity index 86% rename from zero.Core/Application/Application.cs rename to zero.Core/Applications/Application.cs index 6526b40a..5275d43c 100644 --- a/zero.Core/Application/Application.cs +++ b/zero.Core/Applications/Application.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Applications; /// -/// An application is a website. zero can host multiple websites at once which share common assets +/// An application is a website or app. zero can host multiple websites at once which share common assets /// [RavenCollection("Applications")] public class Application : ZeroEntity diff --git a/zero.Core/Application/ApplicationOptions.cs b/zero.Core/Applications/ApplicationOptions.cs similarity index 82% rename from zero.Core/Application/ApplicationOptions.cs rename to zero.Core/Applications/ApplicationOptions.cs index ff1098c0..2250c5da 100644 --- a/zero.Core/Application/ApplicationOptions.cs +++ b/zero.Core/Applications/ApplicationOptions.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Applications; public class ApplicationOptions { diff --git a/zero.Core/Applications/ApplicationResolver.cs b/zero.Core/Applications/ApplicationResolver.cs new file mode 100644 index 00000000..f07e6890 --- /dev/null +++ b/zero.Core/Applications/ApplicationResolver.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Logging; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System.Security.Claims; + +namespace zero.Applications; + +public class ApplicationResolver : IApplicationResolver +{ + protected IZeroStore Store { get; private set; } + + protected IZeroOptions Options { get; private set; } + + protected ILogger Logger { get; private set; } + + protected IHandlerHolder Handler { get; private set; } + + private IList Apps { get; set; } + + + + public ApplicationResolver(IZeroStore store, IZeroOptions options, ILogger logger, IHandlerHolder handler = null) + { + Store = store; + Options = options; + Logger = logger; + Handler = handler; + } + + + /// + public async Task Resolve(HttpContext context, ClaimsPrincipal user) + { + if (context?.Request == null) + { + Logger.LogWarning("Could not resolve application as HTTP request is null"); + return null; + } + + Application app; + + if (context.IsBackofficeRequest(Options.BackofficePath)) + { + app = await ResolveFromUser(user); + } + else + { + app = await ResolveFromRequest(context); + } + + if (app == null) + { + //Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host); + IList apps = await GetApplications(); + app = apps.FirstOrDefault(); + } + + return app; + } + + + /// + public async Task ResolveFromUser(ClaimsPrincipal user) + { + BackofficeUser userEntity = await GetBackofficeUser(user); + return await ResolveFromUser(userEntity); + } + + + /// + public async Task ResolveFromUser(BackofficeUser user) + { + if (user == null) + { + return null; + } + + string appId; + string[] allowedAppIds = user.GetAllowedAppIds(); + + if (!user.CurrentAppId.IsNullOrEmpty()) + { + if (user.IsSuper || allowedAppIds.Contains(user.CurrentAppId, StringComparer.InvariantCultureIgnoreCase)) + { + appId = user.CurrentAppId; + } + else + { + appId = user.AppId; + } + } + else + { + appId = user.AppId; + } + + if (appId.IsNullOrEmpty()) + { + throw new Exception($"User entity ${user.Id} needs a valid AppId"); + } + + IAsyncDocumentSession session = Store.Session(global: true); + return await session.LoadAsync(appId); + } + + + /// + public async Task ResolveFromRequest(HttpContext context) + { + IApplicationResolverHandler handler = Handler.Get(); + Application app = handler?.Resolve(context.Request, await GetApplications()); + + if (app == null) + { + app = await ResolveFromUri(context.Request.GetEncodedUrl()); + } + + return app; + } + + + /// + public async Task ResolveFromUri(string uriString) + { + return ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications()); + } + + + /// + public async Task ResolveFromUri(Uri uri) + { + return ResolveFromUriInternal(uri, await GetApplications()); + } + + + /// + /// Get matching application from an URI + /// + Application ResolveFromUriInternal(Uri uri, IList apps) + { + foreach (Application app in apps) + { + if (app.Domains?.Length < 1) + { + Logger.LogWarning("No domains specified for app {app}", app.Id); + continue; + } + + foreach (Uri domain in app.Domains) + { + int compareResult = Uri.Compare(uri, domain, UriComponents.HostAndPort, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase); + if (compareResult == 0) + { + return app; + } + } + } + + return null; + } + + + /// + /// Get all applications to choose from + /// + async Task> GetApplications() + { + if (Apps != null) + { + return Apps; + } + + IAsyncDocumentSession session = Store.Session(global: true); + Apps = await session.Query().ToListAsync(); + return Apps; + } + + + /// + /// Get backoffice user from claims principal + /// + async Task GetBackofficeUser(ClaimsPrincipal user) + { + string userId = user.FindFirstValue(Constants.Auth.Claims.UserId); + + IAsyncDocumentSession session = Store.Session(global: true); + return await session.LoadAsync(userId); + } +} + + +public interface IApplicationResolver +{ + /// + /// Resolves the current application from either the backoffice user (in case it is backoffice request) + /// or the domain (in case it is frontend request). + /// + Task Resolve(HttpContext context, ClaimsPrincipal user); + + /// + /// Resolves the current application from the request path + /// + Task ResolveFromRequest(HttpContext context); + + /// + /// Get matching application from an URI string + /// + Task ResolveFromUri(string uriString); + + /// + /// Get matching application from an URI + /// + Task ResolveFromUri(Uri uri); + + /// + /// Resolves the current application from the logged-in backoffice user. + /// This method won't return apps the user has no access to. + /// + Task ResolveFromUser(ClaimsPrincipal user); + + /// + /// Resolves the current application from a user. + /// This method won't return apps the user has no access to. + /// + Task ResolveFromUser(BackofficeUser user); +} \ No newline at end of file diff --git a/zero.Core/Applications/ApplicationValidator.cs b/zero.Core/Applications/ApplicationValidator.cs new file mode 100644 index 00000000..b94e8c91 --- /dev/null +++ b/zero.Core/Applications/ApplicationValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace zero.Applications; + +public class ApplicationValidator : ZeroValidator +{ + public ApplicationValidator() + { + //RuleFor(x => x.Code).Length(2); + RuleFor(x => x.Name).NotEmpty().Length(2, 50); + RuleFor(x => x.FullName).MaximumLength(120); + RuleFor(x => x.Email).Email().NotEmpty().MaximumLength(120); + RuleFor(x => x.Domains).NotEmpty(); + } +} diff --git a/zero.Core/Applications/IApplicationResolverHandler.cs b/zero.Core/Applications/IApplicationResolverHandler.cs new file mode 100644 index 00000000..8c2a1d50 --- /dev/null +++ b/zero.Core/Applications/IApplicationResolverHandler.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace zero.Applications; + +public interface IApplicationResolverHandler : IHandler +{ + Application Resolve(HttpRequest request, IEnumerable applications); +} \ No newline at end of file diff --git a/zero.Core/Applications/Module.cs b/zero.Core/Applications/Module.cs new file mode 100644 index 00000000..9edd3587 --- /dev/null +++ b/zero.Core/Applications/Module.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Applications; + +internal class ApplicationsModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscovery.cs b/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscovery.cs index 89ab072b..c1ffceaf 100644 --- a/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscovery.cs +++ b/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscovery.cs @@ -1,12 +1,9 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -namespace zero; +namespace zero.Architecture; public class AssemblyDiscovery : IAssemblyDiscovery { diff --git a/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscoveryContext.cs b/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscoveryContext.cs index 51386f58..33cd0836 100644 --- a/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscoveryContext.cs +++ b/zero.Core/Architecture/AssemblyDiscovery/AssemblyDiscoveryContext.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace zero; +namespace zero.Architecture; public class AssemblyDiscoveryContext { diff --git a/zero.Core/Architecture/AssemblyDiscovery/IAssemblyDiscoveryRule.cs b/zero.Core/Architecture/AssemblyDiscovery/IAssemblyDiscoveryRule.cs index 16bde55c..d2f4a5ab 100644 --- a/zero.Core/Architecture/AssemblyDiscovery/IAssemblyDiscoveryRule.cs +++ b/zero.Core/Architecture/AssemblyDiscovery/IAssemblyDiscoveryRule.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyModel; -namespace zero; +namespace zero.Architecture; public interface IAssemblyDiscoveryRule { diff --git a/zero.Core/Architecture/AssemblyDiscovery/ZeroAssemblyDiscoveryRule.cs b/zero.Core/Architecture/AssemblyDiscovery/ZeroAssemblyDiscoveryRule.cs new file mode 100644 index 00000000..be1f7ff7 --- /dev/null +++ b/zero.Core/Architecture/AssemblyDiscovery/ZeroAssemblyDiscoveryRule.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyModel; + +namespace zero.Architecture; + +public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule +{ + const string ZERO_PREFIX = "zero."; + + /// + public bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context) + { + StringComparison casing = StringComparison.OrdinalIgnoreCase; + // TODO we need to auto-add assemblies and discover their types which have implementations of IZeroPlugin + return library.Name.StartsWith(ZERO_PREFIX, casing) || (context.HasEntryAssembly && library.Name.Contains(context.EntryAssemblyName, casing)); + } +} \ No newline at end of file diff --git a/zero.Core/Abstractions/IZeroRouteEntity.cs b/zero.Core/Architecture/BaseEntities/IZeroRouteEntity.cs similarity index 88% rename from zero.Core/Abstractions/IZeroRouteEntity.cs rename to zero.Core/Architecture/BaseEntities/IZeroRouteEntity.cs index 6528b5ac..91dd72a5 100644 --- a/zero.Core/Abstractions/IZeroRouteEntity.cs +++ b/zero.Core/Architecture/BaseEntities/IZeroRouteEntity.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Architecture; public interface IZeroRouteEntity { diff --git a/zero.Core/Abstractions/ZeroEntity.cs b/zero.Core/Architecture/BaseEntities/ZeroEntity.cs similarity index 98% rename from zero.Core/Abstractions/ZeroEntity.cs rename to zero.Core/Architecture/BaseEntities/ZeroEntity.cs index 5c0c20c3..1b0723ca 100644 --- a/zero.Core/Abstractions/ZeroEntity.cs +++ b/zero.Core/Architecture/BaseEntities/ZeroEntity.cs @@ -1,8 +1,7 @@ using Newtonsoft.Json; -using System; using System.Diagnostics; -namespace zero; +namespace zero.Architecture; [DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")] public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity diff --git a/zero.Core/Abstractions/ZeroIdEntity.cs b/zero.Core/Architecture/BaseEntities/ZeroIdEntity.cs similarity index 78% rename from zero.Core/Abstractions/ZeroIdEntity.cs rename to zero.Core/Architecture/BaseEntities/ZeroIdEntity.cs index 0efadec4..976887a5 100644 --- a/zero.Core/Abstractions/ZeroIdEntity.cs +++ b/zero.Core/Architecture/BaseEntities/ZeroIdEntity.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Architecture; public class ZeroIdEntity { diff --git a/zero.Core/Architecture/Blueprints/Blueprint.cs b/zero.Core/Architecture/Blueprints/Blueprint.cs index 5662193c..1a827ec2 100644 --- a/zero.Core/Architecture/Blueprints/Blueprint.cs +++ b/zero.Core/Architecture/Blueprints/Blueprint.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; -namespace zero; +namespace zero.Architecture; /// /// diff --git a/zero.Core/Architecture/Blueprints/BlueprintConfiguration.cs b/zero.Core/Architecture/Blueprints/BlueprintConfiguration.cs index 30dcf344..494e5ca4 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintConfiguration.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintConfiguration.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Architecture; /// /// Defines a base entity which is synced and properties which are overridden diff --git a/zero.Core/Architecture/Blueprints/BlueprintField.cs b/zero.Core/Architecture/Blueprints/BlueprintField.cs index 9369be1c..dd47a5e4 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintField.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintField.cs @@ -1,8 +1,7 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -namespace zero; +namespace zero.Architecture; public class BlueprintField where T : ZeroEntity { diff --git a/zero.Core/Architecture/Blueprints/BlueprintOptions.cs b/zero.Core/Architecture/Blueprints/BlueprintOptions.cs index d6f74ddb..daddb9ea 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintOptions.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintOptions.cs @@ -1,6 +1,4 @@ -using System; - -namespace zero; +namespace zero.Architecture; public class BlueprintOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Architecture/Blueprints/BlueprintService.cs b/zero.Core/Architecture/Blueprints/BlueprintService.cs index 53a9329b..3f4d7618 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintService.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintService.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace zero; +namespace zero.Architecture; public class BlueprintService : IBlueprintService { diff --git a/zero.Core/Architecture/Module.cs b/zero.Core/Architecture/Module.cs new file mode 100644 index 00000000..246a588b --- /dev/null +++ b/zero.Core/Architecture/Module.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Applications; + +internal class ArchitectureModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + //config.Services.AddScoped(); + //config.Services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Architecture/ZeroModule.cs b/zero.Core/Architecture/Modules/ZeroModule.cs similarity index 82% rename from zero.Core/Architecture/ZeroModule.cs rename to zero.Core/Architecture/Modules/ZeroModule.cs index ee1adf77..4d701b07 100644 --- a/zero.Core/Architecture/ZeroModule.cs +++ b/zero.Core/Architecture/Modules/ZeroModule.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Architecture; public class ZeroModule { diff --git a/zero.Core/Architecture/ZeroModuleConfiguration.cs b/zero.Core/Architecture/Modules/ZeroModuleConfiguration.cs similarity index 94% rename from zero.Core/Architecture/ZeroModuleConfiguration.cs rename to zero.Core/Architecture/Modules/ZeroModuleConfiguration.cs index 5ba47b57..0c3fedd9 100644 --- a/zero.Core/Architecture/ZeroModuleConfiguration.cs +++ b/zero.Core/Architecture/Modules/ZeroModuleConfiguration.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace zero; +namespace zero.Architecture; public class ZeroModuleConfiguration : IZeroModuleConfiguration { diff --git a/zero.Core/Architecture/Plugins/IZeroBuiltInPlugin.cs b/zero.Core/Architecture/Plugins/IZeroBuiltInPlugin.cs new file mode 100644 index 00000000..9221235b --- /dev/null +++ b/zero.Core/Architecture/Plugins/IZeroBuiltInPlugin.cs @@ -0,0 +1,6 @@ +namespace zero.Architecture; + +internal interface IZeroBuiltInPlugin +{ + +} \ No newline at end of file diff --git a/zero.Core/Architecture/Plugins/IZeroPlugin.cs b/zero.Core/Architecture/Plugins/IZeroPlugin.cs new file mode 100644 index 00000000..cb84696b --- /dev/null +++ b/zero.Core/Architecture/Plugins/IZeroPlugin.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Architecture; + +public abstract class ZeroPlugin : IZeroPlugin +{ + public IZeroPluginOptions Options { get; } = new ZeroPluginOptions(); + + public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { } + + public virtual void Configure(IZeroOptions zero) { } +} + + +public interface IZeroPlugin +{ + IZeroPluginOptions Options { get; } + + void ConfigureServices(IServiceCollection services, IConfiguration configuration); + + void Configure(IZeroOptions zero); +} \ No newline at end of file diff --git a/zero.Core/Architecture/Plugins/ZeroPluginOptions.cs b/zero.Core/Architecture/Plugins/ZeroPluginOptions.cs new file mode 100644 index 00000000..a273812f --- /dev/null +++ b/zero.Core/Architecture/Plugins/ZeroPluginOptions.cs @@ -0,0 +1,30 @@ +namespace zero.Architecture; + +public class ZeroPluginOptions : IZeroPluginOptions +{ + public string Name { get; set; } + + public string Description { get; set; } + + public List LocalizationPaths { get; private set; } = new List(); + + public string PluginPath { get; set; } +} + + +public interface IZeroPluginOptions +{ + string Name { get; set; } + + string Description { get; set; } + + List LocalizationPaths { get; } + + string PluginPath { get; set; } +} + + +public interface IZeroPluginStartup +{ + Task Startup(); +} \ No newline at end of file diff --git a/zero.Core/Communication/Handlers/IHandler.cs b/zero.Core/Communication/Handlers/IHandler.cs new file mode 100644 index 00000000..c2ea5d02 --- /dev/null +++ b/zero.Core/Communication/Handlers/IHandler.cs @@ -0,0 +1,5 @@ +namespace zero.Communication; + +public interface IHandler +{ +} diff --git a/zero.Core/Communication/Handlers/IHandlerHolder.cs b/zero.Core/Communication/Handlers/IHandlerHolder.cs new file mode 100644 index 00000000..16119a7e --- /dev/null +++ b/zero.Core/Communication/Handlers/IHandlerHolder.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Communication; + +public class HandlerHolder : IHandlerHolder +{ + IServiceProvider Services { get; } + + public HandlerHolder(IServiceProvider services) + { + Services = services; + } + + + public T Get() where T : IHandler + { + return Services.GetService(); + } +} + +public interface IHandlerHolder +{ + T Get() where T : IHandler; +} \ No newline at end of file diff --git a/zero.Core/Communication/Handlers/IModuleTypeHandler.cs b/zero.Core/Communication/Handlers/IModuleTypeHandler.cs new file mode 100644 index 00000000..e42ef0e3 --- /dev/null +++ b/zero.Core/Communication/Handlers/IModuleTypeHandler.cs @@ -0,0 +1,6 @@ +namespace zero.Communication; + +public interface IModuleTypeHandler : IHandler +{ + IEnumerable GetAllowedModuleTypes(Application application, IEnumerable registeredTypes, Page page = default, string[] tags = default); +} diff --git a/zero.Core/Communication/Interceptors/CollectionInterceptor.cs b/zero.Core/Communication/Interceptors/CollectionInterceptor.cs new file mode 100644 index 00000000..e545ea9e --- /dev/null +++ b/zero.Core/Communication/Interceptors/CollectionInterceptor.cs @@ -0,0 +1,88 @@ +namespace zero.Communication; + +public abstract partial class CollectionInterceptor : ICollectionInterceptor where T : ZeroIdEntity +{ + /// + public virtual bool CanRun(InterceptorParameters args, T model) => true; + + /// + public virtual Task> Creating(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public virtual Task> Updating(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public virtual Task> Saving(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public virtual Task> Deleting(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public virtual Task Saved(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask; +} + + +public abstract partial class CollectionInterceptor : CollectionInterceptor, ICollectionInterceptor +{ + /// + public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) => base.CanRun(args, model); +} + +public interface ICollectionInterceptor : ICollectionInterceptor { } + +public interface ICollectionInterceptor where T : ZeroIdEntity +{ + /// + /// Whether any of the interceptor methods is allowed to run based on the parameters + /// + bool CanRun(InterceptorParameters args, T model); + + /// + /// Called after an entity has been stored but before the session has saved its changes + /// + Task Created(InterceptorParameters args, T model); + + /// + /// Called before an entity is stored and validated + /// + Task> Creating(InterceptorParameters args, T model); + + /// + /// Called after an entity has been deleted but before the session has saved its changes + /// + Task Deleted(InterceptorParameters args, T model); + + /// + /// Called before an entity is deleted + /// + Task> Deleting(InterceptorParameters args, T model); + + /// + /// Called after an entity has been updated but before the session has saved its changes + /// + Task Updated(InterceptorParameters args, T model); + + /// + /// Called before an entity is stored and validated + /// + Task> Updating(InterceptorParameters args, T model); + + /// + /// Called after an entity has been saved (created or updated) but before the session has saved its changes + /// + Task Saved(InterceptorParameters args, T model); + + /// + /// Called before an entity is stored and validated + /// + Task> Saving(InterceptorParameters args, T model); +} \ No newline at end of file diff --git a/old/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs b/zero.Core/Communication/Interceptors/CollectionInterceptorHandler.cs similarity index 100% rename from old/zero.Core/Collections/Interceptors/CollectionInterceptorHandler.cs rename to zero.Core/Communication/Interceptors/CollectionInterceptorHandler.cs diff --git a/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs b/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs new file mode 100644 index 00000000..4f504d67 --- /dev/null +++ b/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs @@ -0,0 +1,49 @@ +namespace zero.Communication; + +public sealed class CollectionInterceptorShim : CollectionInterceptor where T : ZeroIdEntity +{ + ICollectionInterceptor _base; + + public CollectionInterceptorShim(ICollectionInterceptor baseInterceptor) + { + _base = baseInterceptor; + } + + InterceptorResult Result(InterceptorResult result) + { + return result == null ? null : new InterceptorResult() + { + InterceptorHash = result.InterceptorHash, + Parameters = result.Parameters, + Prevent = result.Prevent, + Result = result.Result != null ? EntityResult.From(result.Result, result.Result.Model as T) : null + }; + } + + /// + public override bool CanRun(InterceptorParameters args, T model) => _base.CanRun(args, model); + + /// + public override async Task> Creating(InterceptorParameters args, T model) => Result(await _base.Creating(args, model)); + + /// + public override async Task> Updating(InterceptorParameters args, T model) => Result(await _base.Updating(args, model)); + + /// + public override async Task> Saving(InterceptorParameters args, T model) => Result(await _base.Saving(args, model)); + + /// + public override async Task> Deleting(InterceptorParameters args, T model) => Result(await _base.Deleting(args, model)); + + /// + public override Task Created(InterceptorParameters args, T model) => _base.Created(args, model); + + /// + public override Task Updated(InterceptorParameters args, T model) => _base.Updated(args, model); + + /// + public override Task Saved(InterceptorParameters args, T model) => _base.Saved(args, model); + + /// + public override Task Deleted(InterceptorParameters args, T model) => _base.Deleted(args, model); +} diff --git a/old/zero.Core/Collections/Interceptors/InterceptorInstruction.cs b/zero.Core/Communication/Interceptors/InterceptorInstruction.cs similarity index 94% rename from old/zero.Core/Collections/Interceptors/InterceptorInstruction.cs rename to zero.Core/Communication/Interceptors/InterceptorInstruction.cs index 30de66ae..96b94b85 100644 --- a/old/zero.Core/Collections/Interceptors/InterceptorInstruction.cs +++ b/zero.Core/Communication/Interceptors/InterceptorInstruction.cs @@ -1,9 +1,4 @@ -namespace zero.Core; - -using System; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Entities; +namespace zero.Communication; public class InterceptorInstruction where T : ZeroIdEntity, new() { diff --git a/old/zero.Core/Collections/Interceptors/InterceptorInstructionProxy.cs b/zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs similarity index 85% rename from old/zero.Core/Collections/Interceptors/InterceptorInstructionProxy.cs rename to zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs index 31490521..dac5357e 100644 --- a/old/zero.Core/Collections/Interceptors/InterceptorInstructionProxy.cs +++ b/zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs @@ -1,6 +1,4 @@ -namespace zero.Core; -using System.Threading.Tasks; -using zero.Core.Entities; +namespace zero.Communication; public class InterceptorRunnerProxy { diff --git a/old/zero.Core/Collections/Interceptors/InterceptorParameters.cs b/zero.Core/Communication/Interceptors/InterceptorParameters.cs similarity index 89% rename from old/zero.Core/Collections/Interceptors/InterceptorParameters.cs rename to zero.Core/Communication/Interceptors/InterceptorParameters.cs index 8bc4b35b..5cd1e1fe 100644 --- a/old/zero.Core/Collections/Interceptors/InterceptorParameters.cs +++ b/zero.Core/Communication/Interceptors/InterceptorParameters.cs @@ -1,10 +1,4 @@ -namespace zero.Core; - -using System.Collections.Generic; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; - +namespace zero.Communication; public class InterceptorParameters : InterceptorParameters where T : ZeroIdEntity, new() { diff --git a/zero.Core/Communication/Interceptors/InterceptorResult.cs b/zero.Core/Communication/Interceptors/InterceptorResult.cs new file mode 100644 index 00000000..af6a1ff3 --- /dev/null +++ b/zero.Core/Communication/Interceptors/InterceptorResult.cs @@ -0,0 +1,24 @@ +namespace zero.Communication; + +public class InterceptorResult +{ + /// + /// Autoset. Hash used to match interceptors in order to correctly pass parameters + /// + internal string InterceptorHash { get; set; } + + /// + /// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...) + /// + public bool Prevent { get; set; } + + /// + /// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors + /// + public EntityResult Result { get; set; } + + /// + /// Additional parameters which can be passed to the interceptor after the operation was completed + /// + public Dictionary Parameters { get; set; } = new(); +} \ No newline at end of file diff --git a/old/zero.Core/Collections/Interceptors/InterceptorRunner.cs b/zero.Core/Communication/Interceptors/InterceptorRunner.cs similarity index 91% rename from old/zero.Core/Collections/Interceptors/InterceptorRunner.cs rename to zero.Core/Communication/Interceptors/InterceptorRunner.cs index ac196505..0a0f5c77 100644 --- a/old/zero.Core/Collections/Interceptors/InterceptorRunner.cs +++ b/zero.Core/Communication/Interceptors/InterceptorRunner.cs @@ -1,11 +1,6 @@ -namespace zero.Core; +using System.Collections.Concurrent; -using System; -using System.Collections.Concurrent; -using System.Linq; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Options; +namespace zero.Communication; public class InterceptorRunner : IInterceptorRunner where T : ZeroIdEntity, new() { diff --git a/old/zero.Core/Collections/Interceptors/InterceptorType.cs b/zero.Core/Communication/Interceptors/InterceptorType.cs similarity index 72% rename from old/zero.Core/Collections/Interceptors/InterceptorType.cs rename to zero.Core/Communication/Interceptors/InterceptorType.cs index 7bcd2e12..f1260abb 100644 --- a/old/zero.Core/Collections/Interceptors/InterceptorType.cs +++ b/zero.Core/Communication/Interceptors/InterceptorType.cs @@ -1,4 +1,4 @@ -namespace zero.Core; +namespace zero.Communication; public enum InterceptorType { diff --git a/zero.Core/Communication/Messages/IMessage.cs b/zero.Core/Communication/Messages/IMessage.cs new file mode 100644 index 00000000..4c03d107 --- /dev/null +++ b/zero.Core/Communication/Messages/IMessage.cs @@ -0,0 +1,6 @@ + +namespace zero.Communication; + +public interface IMessage +{ +} diff --git a/zero.Core/Communication/Messages/IMessageHandler.cs b/zero.Core/Communication/Messages/IMessageHandler.cs new file mode 100644 index 00000000..a818e1de --- /dev/null +++ b/zero.Core/Communication/Messages/IMessageHandler.cs @@ -0,0 +1,26 @@ +namespace zero.Communication; + +/// +/// Indicates a handler that can perform an action when a message of +/// a certain type is received. (could be an interface rather than concrete type) +/// +public interface IMessageHandler where TMessage : IMessage +{ + /// + /// Method to invoke when a message of type TMessage is published + /// + Task Handle(TMessage message); +} + + +/// +/// Indicates a handler that can perform an action when a message of +/// a certain type is received. (could be an interface rather than concrete type) +/// +public interface IBatchMessageHandler : IMessageHandler where TMessage : IMessage +{ + /// + /// Method to invoke when a batch of messages of type TMessage are published + /// + Task HandleBatchAsync(IReadOnlyCollection message); +} diff --git a/zero.Core/Communication/Messages/MessageAggregator.cs b/zero.Core/Communication/Messages/MessageAggregator.cs new file mode 100644 index 00000000..c6615c79 --- /dev/null +++ b/zero.Core/Communication/Messages/MessageAggregator.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; + +namespace zero.Communication; + +public class MessageAggregator : IMessageAggregator +{ + readonly ConcurrentBag Subscription = new ConcurrentBag(); + readonly IServiceProvider ServiceProvider; + + + public MessageAggregator(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + + /// + public async Task Publish(TMessage message) where TMessage : class, IMessage + { + IEnumerable subscriptions = Subscription.Where(s => s.CanDeliver()); + + foreach (IMessageSubscription subscription in subscriptions) + { + await subscription.Deliver(ServiceProvider, message); + } + } + + + /// + public void Subscribe() + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler + { + Subscription.Add(new MessageSubscription()); + } +} + + +public interface IMessageAggregator +{ + /// + /// Publishes the specified message, invoking any handlers subscribed to the message. + /// + Task Publish(TMessage message) where TMessage : class, IMessage; + + /// + /// Subscribes the specified handler to the spified message type + /// + void Subscribe() + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler; +} diff --git a/zero.Core/Communication/Messages/MessageSubscription.cs b/zero.Core/Communication/Messages/MessageSubscription.cs new file mode 100644 index 00000000..ace0d9ff --- /dev/null +++ b/zero.Core/Communication/Messages/MessageSubscription.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace zero.Communication; + +internal class MessageSubscription : IMessageSubscription + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler +{ + public bool CanDeliver() + { + return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T)); + } + + public async Task Deliver(IServiceProvider serviceProvider, object message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + if (!(message is TMessage)) + { + throw new ArgumentException($"{ nameof(message) } must be of type '{typeof(TMessage).FullName}'"); + } + + var handler = serviceProvider.GetRequiredService(); + await handler.Handle((TMessage)message); + } +} + + +public interface IMessageSubscription +{ + bool CanDeliver(); + + Task Deliver(IServiceProvider serviceProvider, object message); +} diff --git a/zero.Core/Communication/Module.cs b/zero.Core/Communication/Module.cs new file mode 100644 index 00000000..1f767c84 --- /dev/null +++ b/zero.Core/Communication/Module.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Applications; + +internal class CommunicationModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddSingleton(); + } +} \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs b/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs index ed683b7a..ac17585d 100644 --- a/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs @@ -1,6 +1,6 @@ using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class FeatureOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/IconOptions.cs b/zero.Core/Configuration/ConfigurationParts/IconOptions.cs index 922ce6dc..1b8d5eaf 100644 --- a/zero.Core/Configuration/ConfigurationParts/IconOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/IconOptions.cs @@ -1,6 +1,6 @@ using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class IconOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs b/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs index dc342225..9ee6199e 100644 --- a/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using zero.Core.Integrations; -namespace zero; +namespace zero.Configuration; public class IntegrationOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs b/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs index 65ce2710..43fbcae0 100644 --- a/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs @@ -1,7 +1,7 @@ using System; using zero.Core.Collections; -namespace zero; +namespace zero.Configuration; public class InterceptorOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/ModuleOptions.cs b/zero.Core/Configuration/ConfigurationParts/ModuleOptions.cs index ea216547..b2682a5c 100644 --- a/zero.Core/Configuration/ConfigurationParts/ModuleOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/ModuleOptions.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class ModuleOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/PageOptions.cs b/zero.Core/Configuration/ConfigurationParts/PageOptions.cs index ecca8e2d..132f1664 100644 --- a/zero.Core/Configuration/ConfigurationParts/PageOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/PageOptions.cs @@ -2,7 +2,7 @@ using System.Linq; using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class PageOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs b/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs index 0a3bc928..1ac2caeb 100644 --- a/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using zero.Core.Identity; -namespace zero; +namespace zero.Configuration; public class PermissionOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs b/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs index 5be917b1..fde2706a 100644 --- a/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using zero.Core.Models; using zero.Core.Renderer; -namespace zero; +namespace zero.Configuration; public class SearchOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs b/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs index ec02e450..021f61ec 100644 --- a/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs @@ -1,6 +1,6 @@ using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class SectionOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/ServiceOptions.cs b/zero.Core/Configuration/ConfigurationParts/ServiceOptions.cs index 0861d1b6..ddecf6fc 100644 --- a/zero.Core/Configuration/ConfigurationParts/ServiceOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/ServiceOptions.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Configuration; public class ServiceOptions { diff --git a/zero.Core/Configuration/ConfigurationParts/SettingsOptions.cs b/zero.Core/Configuration/ConfigurationParts/SettingsOptions.cs index 17c1f6b1..9b431d31 100644 --- a/zero.Core/Configuration/ConfigurationParts/SettingsOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/SettingsOptions.cs @@ -1,7 +1,7 @@ using System.Linq; using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class SettingsOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/SpaceOptions.cs b/zero.Core/Configuration/ConfigurationParts/SpaceOptions.cs index ab457712..f0f789ea 100644 --- a/zero.Core/Configuration/ConfigurationParts/SpaceOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/SpaceOptions.cs @@ -1,7 +1,7 @@ using System.Linq; using zero.Core.Entities; -namespace zero; +namespace zero.Configuration; public class SpaceOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/Constants.cs b/zero.Core/Configuration/Constants.cs index 540e57ed..d99d12f9 100644 --- a/zero.Core/Configuration/Constants.cs +++ b/zero.Core/Configuration/Constants.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Configuration; public static class Constants { diff --git a/zero.Core/Configuration/IZeroCollectionOptions.cs b/zero.Core/Configuration/IZeroCollectionOptions.cs index e4ecc6c5..1bd70b9e 100644 --- a/zero.Core/Configuration/IZeroCollectionOptions.cs +++ b/zero.Core/Configuration/IZeroCollectionOptions.cs @@ -1,7 +1,6 @@ -namespace zero.Core.Options +namespace zero.Configuration; + +public interface IZeroCollectionOptions { - public interface IZeroCollectionOptions - { - } -} +} \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/Integration.cs b/zero.Core/Configuration/Integrations/Integration.cs new file mode 100644 index 00000000..6405a828 --- /dev/null +++ b/zero.Core/Configuration/Integrations/Integration.cs @@ -0,0 +1,12 @@ +namespace zero.Configuration; + +/// +/// An integration is an application part which has a public configuration per app. +/// It's up to the user to provide functionality. +/// +[RavenCollection("Integrations")] +public class Integration : ZeroEntity +{ + /// + public string TypeAlias { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/IntegrationService.cs b/zero.Core/Configuration/Integrations/IntegrationService.cs new file mode 100644 index 00000000..6fc81362 --- /dev/null +++ b/zero.Core/Configuration/Integrations/IntegrationService.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; + +namespace zero.Configuration; + +public class IntegrationService : IntegrationsCollection, IIntegrationService +{ + public IntegrationService(ICollectionContext context, ILogger logger) : base(context, logger) + { + Options = new(false); + } +} + + +public interface IIntegrationService : IIntegrationsCollection { } \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/IntegrationType.cs b/zero.Core/Configuration/Integrations/IntegrationType.cs new file mode 100644 index 00000000..20a25590 --- /dev/null +++ b/zero.Core/Configuration/Integrations/IntegrationType.cs @@ -0,0 +1,51 @@ +namespace zero.Configuration; + +/// +/// An integration is an application part which has a public configuration per app. +/// It's up to the user to provide functionality. +/// +public class IntegrationType : IntegrationType where T : Integration, new() +{ + public IntegrationType() : base(typeof(T)) { } +} + +/// +/// An integration is an application part which has a public configuration per app. +/// It's up to the user to provide functionality. +/// +public class IntegrationType : OptionsType +{ + /// + /// Group integrations by tags + /// + public List Tags { get; set; } = new(); + + /// + /// Optional description + /// + public string Description { get; set; } + + /// + /// Image of the integration + /// + public string ImagePath { get; set; } + + + public IntegrationType(Type type) + { + ContentType = type; + } + + public static IntegrationType Convert(IntegrationType model) where T : Integration, new() + { + return new(model.ContentType) + { + Alias = model.Alias, + Name = model.Name, + Description = model.Description, + ImagePath = model.ImagePath, + Tags = model.Tags, + Validator = model.Validator + }; + } +} diff --git a/zero.Core/Configuration/Integrations/IntegrationTypeWithStatus.cs b/zero.Core/Configuration/Integrations/IntegrationTypeWithStatus.cs new file mode 100644 index 00000000..a95cbb20 --- /dev/null +++ b/zero.Core/Configuration/Integrations/IntegrationTypeWithStatus.cs @@ -0,0 +1,12 @@ +namespace zero.Configuration; + +public class IntegrationTypeWithStatus +{ + public IntegrationType Type { get; set; } + + public bool IsActive { get; set; } + + public bool IsConfigured { get; set; } + + public string Id { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationModule.cs b/zero.Core/Configuration/Module.cs similarity index 94% rename from zero.Core/Configuration/ConfigurationModule.cs rename to zero.Core/Configuration/Module.cs index f7dabe21..fa77ab49 100644 --- a/zero.Core/Configuration/ConfigurationModule.cs +++ b/zero.Core/Configuration/Module.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace zero; +namespace zero.Configuration; internal class ConfigurationModule : ZeroModule { diff --git a/zero.Core/Configuration/OptionsEnumerable.cs b/zero.Core/Configuration/OptionsEnumerable.cs index 4d0382dd..ba517c52 100644 --- a/zero.Core/Configuration/OptionsEnumerable.cs +++ b/zero.Core/Configuration/OptionsEnumerable.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace zero; +namespace zero.Configuration; public abstract class OptionsEnumerable { diff --git a/zero.Core/Configuration/OptionsType.cs b/zero.Core/Configuration/OptionsType.cs index 4ea088e4..44d73a4a 100644 --- a/zero.Core/Configuration/OptionsType.cs +++ b/zero.Core/Configuration/OptionsType.cs @@ -1,7 +1,6 @@ using FluentValidation; -using System; -namespace zero; +namespace zero.Configuration; public abstract class OptionsType { diff --git a/zero.Core/Configuration/ZeroOptions.cs b/zero.Core/Configuration/ZeroOptions.cs index 0302e189..8574c5a7 100644 --- a/zero.Core/Configuration/ZeroOptions.cs +++ b/zero.Core/Configuration/ZeroOptions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Configuration; public class ZeroOptions : IZeroOptions { diff --git a/zero.Core/Configuration/ZeroStartupOptions.cs b/zero.Core/Configuration/ZeroStartupOptions.cs index 63e94723..cc11bcb8 100644 --- a/zero.Core/Configuration/ZeroStartupOptions.cs +++ b/zero.Core/Configuration/ZeroStartupOptions.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using System.Collections.Generic; -namespace zero; +namespace zero.Configuration; public class ZeroStartupOptions : IZeroStartupOptions { diff --git a/zero.Core/Context/ZeroContext.cs b/zero.Core/Context/ZeroContext.cs new file mode 100644 index 00000000..c550c570 --- /dev/null +++ b/zero.Core/Context/ZeroContext.cs @@ -0,0 +1,262 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Security.Claims; + +namespace zero.Context; + +public class ZeroContext : IZeroContext +{ + /// + public Application Application { get; protected set; } + + /// + public string AppId { get; protected set; } + + /// + public ClaimsPrincipal BackofficeUser { get; protected set; } + + /// + public bool IsBackofficeRequest { get; protected set; } + + /// + public bool IsLoggedIntoBackoffice { get; protected set; } + + /// + public IZeroOptions Options { get; protected set; } + + /// + public Route Route => ResolvedRoute?.Route; + + /// + public IRouteModel ResolvedRoute => HttpContextAccessor?.HttpContext?.Features.Get(); + + /// + public IZeroStore Store { get; private set; } + + /// + public IServiceProvider Services { get; private set; } + + + protected IApplicationResolver AppResolver { get; private set; } + + protected ICultureResolver CultureResolver { get; private set; } + + protected ILogger Logger { get; private set; } + + protected IHandlerHolder Handler { get; private set; } + + protected IHttpContextAccessor HttpContextAccessor { get; private set; } + + protected IPrimitiveTypeCollection ValueCollection { get; private set; } + + + bool _resolved = false; + + + public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, IApplicationResolver appResolver, ICultureResolver cultureResolver, + ILogger logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services) + { + Options = options; + AppResolver = appResolver; + CultureResolver = cultureResolver; + Logger = logger; + Store = store; + Handler = handler; + ValueCollection = new PrimitiveTypeCollection(); + HttpContextAccessor = httpContextAccessor; + Services = services; + } + + + /// + public async virtual Task Resolve(HttpContext context) + { + if (_resolved) + { + return; + } + + if (context?.Request is null) + { + Store.ResolvedDatabase = null; + return; + } + + if (!Options.SetupCompleted) + { + return; + } + + _resolved = true; + + // check if the current request is a backoffice request + IsBackofficeRequest = context.IsBackofficeRequest(Options.BackofficePath); + + // get the currently logged-in backoffice user + BackofficeUser = new ClaimsPrincipal(); + IsLoggedIntoBackoffice = false; + AuthenticateResult authResult = await context.AuthenticateAsync(Constants.Auth.BackofficeScheme); + if (authResult?.Principal is not null) + { + BackofficeUser = authResult.Principal; + IsLoggedIntoBackoffice = true; + } + + // resolve current application + Application = await AppResolver.Resolve(context, BackofficeUser); + AppId = Application.Id; + + // set default database for document store + Store.ResolvedDatabase = Application.Database; + + // set current culture + await CultureResolver.Resolve(this); + } + + + /// + public void Override(Application app) + { + Application = app; + AppId = app?.Id; + Store.ResolvedDatabase = app?.Database; + } + + + /// + public ZeroContextScope CreateScope(Application app) + { + return new(this, app, Application); + } + + + /// + public T Get() => ValueCollection.Get(); + + + /// + public void Set(T value) => ValueCollection.Set(value); + + + /// + public void Remove() => ValueCollection.Remove(); +} + + + +public class ZeroContextScope : IDisposable +{ + public IZeroContext Context { get; } + + public Application App { get; set; } + + public string Database { get; set; } + + public IZeroStore Store { get; set; } + + Application _originalApp = null; + + + internal ZeroContextScope(IZeroContext context, Application app, Application originalApp) + { + Context = context; + App = app; + Database = app.Database; + Store = context.Store; + _originalApp = originalApp; + Context.Override(app); + } + + /// + public void Dispose() + { + Context.Override(_originalApp); + } +} + + + +public interface IZeroContext +{ + /// + /// Currently loaded application + /// + Application Application { get; } + + /// + /// Current loaded application Id + /// + string AppId { get; } + + /// + /// Resolved backoffice user principal + /// + ClaimsPrincipal BackofficeUser { get; } + + /// + /// Whether the current request is a backoffice request or not + /// + bool IsBackofficeRequest { get; } + + /// + /// Whether the user is logged into the backoffice + /// + bool IsLoggedIntoBackoffice { get; } + + /// + /// Global zero options + /// + IZeroOptions Options { get; } + + /// + /// Document store + /// + IZeroStore Store { get; } + + /// + /// Service container + /// + IServiceProvider Services { get; } + + /// + /// Matching (frontend) path route + /// + Route Route { get; } + + /// + /// Matching (frontend) resolved route + /// + IRouteModel ResolvedRoute { get; } + + /// + /// Resolves the current application (for backoffice + frontend requests) and + /// the currently active backoffice user, as users are not signed in with the default scheme and do therefore not populate HttpContext.User + /// + Task Resolve(HttpContext context); + + /// + /// Overrides the resolved application for this context instance + /// + void Override(Application app); + + /// + /// SCOPE + /// + ZeroContextScope CreateScope(Application app); + + /// + /// Get a custom property from this scoped context + /// + T Get(); + + /// + /// Add a custom property to this scoped context + /// + void Set(T value); + + /// + /// Remove a custom property from this scoped context + /// + void Remove(); +} \ No newline at end of file diff --git a/zero.Core/Extensions/CharExtensions.cs b/zero.Core/Extensions/CharExtensions.cs index 3cba4ea1..1560fc9b 100644 --- a/zero.Core/Extensions/CharExtensions.cs +++ b/zero.Core/Extensions/CharExtensions.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace zero; +namespace zero.Extensions; public static class CharExtensions { diff --git a/zero.Core/Extensions/ColorExtensions.cs b/zero.Core/Extensions/ColorExtensions.cs index fb3e8c7a..0d35f1d1 100644 --- a/zero.Core/Extensions/ColorExtensions.cs +++ b/zero.Core/Extensions/ColorExtensions.cs @@ -1,8 +1,6 @@ -using System; -using System.Drawing; -using System.Linq; +using System.Drawing; -namespace zero; +namespace zero.Extensions; public static class ColorExtensions { diff --git a/zero.Core/Extensions/DictionaryExtensions.cs b/zero.Core/Extensions/DictionaryExtensions.cs index 23e5d599..6575f3a8 100644 --- a/zero.Core/Extensions/DictionaryExtensions.cs +++ b/zero.Core/Extensions/DictionaryExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Extensions; public static class DictionaryExtensions { diff --git a/zero.Core/Extensions/EnumerableExtensions.cs b/zero.Core/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..dd76d0cb --- /dev/null +++ b/zero.Core/Extensions/EnumerableExtensions.cs @@ -0,0 +1,84 @@ +namespace zero.Extensions; + +public static class EnumerableExtensions +{ + /// + /// + /// + public static ListResult ToQueriedList(this IEnumerable items, ListQuery query) where T : ZeroEntity + { + //queryable = queryable.Statistics(out QueryStatistics stats); + + if (query != null) + { + if (!query.IncludeInactive) + { + items = items.Where(x => x.IsActive); + } + + if (!query.Search.IsNullOrEmpty()) + { + items = items.Where(x => x.Name.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase)); + } + //if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) + //{ + // items = items.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); + //} + //if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) + //{ + // foreach (var selector in query.SearchSelectors) + // { + // items = items.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); + // } + //} + + //if (!query.OrderBy.IsNullOrEmpty()) + //{ + // items = items.OrderBy(query.OrderBy, query.OrderIsDescending); + //} + + if (query.PageSize > 0) + { + items = items.Paging(query.Page, query.PageSize); + } + } + + List result = items.ToList(); + + return new ListResult(result, result.Count, query.Page, query.PageSize); + } + + + + public static IEnumerable Paging(this IEnumerable source, int pageNumber, int pageSize) + { + pageNumber = pageNumber.Limit(1, 10_000_000); + pageSize = pageSize.Limit(1, 1_000); + + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + + + public static bool TryGet(this IEnumerable source, Func predicate, out T model) + { + model = source.FirstOrDefault(predicate); + return model != null; + } + + + public static bool TryAdd(this IList source, T model) where T : class + { + if (model == default) + { + return false; + } + + source.Add(model); + return true; + } +} \ No newline at end of file diff --git a/zero.Core/Extensions/ExpressionExtensions.cs b/zero.Core/Extensions/ExpressionExtensions.cs index e680a859..589fbc25 100644 --- a/zero.Core/Extensions/ExpressionExtensions.cs +++ b/zero.Core/Extensions/ExpressionExtensions.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Text; -namespace zero; +namespace zero.Extensions; public static class ExpressionExtensions { diff --git a/zero.Core/Extensions/HttpContextExtensions.cs b/zero.Core/Extensions/HttpContextExtensions.cs new file mode 100644 index 00000000..5eed83cf --- /dev/null +++ b/zero.Core/Extensions/HttpContextExtensions.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Http; + +namespace zero.Extensions; + +public static class HttpContextExtensions +{ + /// + /// Whether the current request is a backoffice request + /// + public static bool IsBackofficeRequest(this HttpContext context, string backofficePath) + { + string path = backofficePath.EnsureStartsWith('/').TrimEnd('/'); + return context.Request.Path.ToString().StartsWith(path); + } + + + /// + /// Whether the current request is an AJAX request + /// + public static bool IsAjaxRequest(this HttpContext context) + { + if (context?.Request?.Headers == null) + { + return false; + } + + return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest"; + } + + + /// + /// Whether the current request only accepts application/json + /// + public static bool IsJsonRequest(this HttpContext context) + { + if (context?.Request?.Headers == null) + { + return false; + } + + return context.Request.Headers["Accept"] == "application/json"; + } + + + /// + /// Get IP Address of the client + /// + public static string GetClientIpAddress(this HttpContext context) + { + return context.Connection.RemoteIpAddress?.ToString(); + //string ipAddress = context.GetServerVariable("HTTP_X_FORWARDED_FOR"); + //return !ipAddress.IsNullOrEmpty() ? ipAddress.Split(',')[0] : context.GetServerVariable("REMOTE_ADDR"); + } +} diff --git a/zero.Core/Extensions/NumberExtensions.cs b/zero.Core/Extensions/NumberExtensions.cs index 031485ee..26f13f4f 100644 --- a/zero.Core/Extensions/NumberExtensions.cs +++ b/zero.Core/Extensions/NumberExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Extensions; public static class NumberExtensions { diff --git a/zero.Core/Extensions/ObjectExtensions.cs b/zero.Core/Extensions/ObjectExtensions.cs new file mode 100644 index 00000000..25ef9f9a --- /dev/null +++ b/zero.Core/Extensions/ObjectExtensions.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace zero.Extensions; + +[Obsolete("we don't want this for every object (use a Utils class instead)")] +public static class ObjectExtensions +{ + public static bool Is(this Type type) + { + return type.IsAssignableFrom(typeof(T)); + } + + + public static bool Is(this object obj) + { + return obj.GetType().IsAssignableFrom(typeof(T)); + } + + + public static T Clone(this T obj) + { + Type type = obj.GetType(); + return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), type, new RefJsonConverter()); + } + + public static T CloneLax(this object obj) + { + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), new RefJsonConverter()); + } + + public static T AutoSetIds(this T obj) + { + // find all Raven Ids + List> ravenIds = ObjectTraverser.FindAttribute(obj); + + // set unset Raven Ids + foreach (ObjectTraverser.Result item in ravenIds) + { + string id = item.Property.GetValue(item.Parent, null) as string; + if (String.IsNullOrWhiteSpace(id)) + { + item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create()); + } + } + + return obj; + } +} diff --git a/zero.Core/Extensions/QueryableExtensions.cs b/zero.Core/Extensions/QueryableExtensions.cs new file mode 100644 index 00000000..249933dd --- /dev/null +++ b/zero.Core/Extensions/QueryableExtensions.cs @@ -0,0 +1,103 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Queries; +using Raven.Client.Documents.Session; +using System.Linq.Expressions; + +namespace zero.Extensions; + +public static class QueryableExtensions +{ + public static IOrderedQueryable OrderBy(this IQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) + { + if (!String.IsNullOrEmpty(path)) + { + char[] a = path.ToCharArray(); + a[0] = char.ToUpper(a[0]); + path = new string(a); + } + + if (isDescending) + { + return source.OrderByDescending(path, type); + } + return source.OrderBy(path, type); + } + + + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) + { + if (!String.IsNullOrEmpty(path)) + { + char[] a = path.ToCharArray(); + a[0] = char.ToUpper(a[0]); + path = new string(a); + } + + if (isDescending) + { + return source.ThenByDescending(path, type); + } + return source.ThenBy(path, type); + } + + + public static IQueryable Paging(this IQueryable source, int pageNumber, int pageSize) + { + pageNumber = pageNumber.Limit(1, 10_000_000); + pageSize = pageSize.Limit(1, 1_000); + + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + + + public static IRavenQueryable WhereIf(this IRavenQueryable source, Expression> predicate, bool condition, Expression> elsePredicate = null) + { + if (!condition) + { + if (elsePredicate != null) + { + return source.Where(elsePredicate); + } + return source; + } + + return source.Where(predicate); + } + + + public static IQueryable SearchIf(this IQueryable source, Expression> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or) + { + if (String.IsNullOrWhiteSpace(searchTerms)) + { + return source; + } + + searchTerms = searchTerms.Trim(); + + string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => + { + if (suffix != null) + { + x += suffix; + } + if (prefix != null) + { + x = prefix + x; + } + return x; + }).ToArray(); + + if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"')) + { + searchParts = new[] { searchTerms }; + } + + return source.Search(fieldSelector, searchParts, @operator: @operator); + } +} diff --git a/zero.Core/Extensions/ServiceCollectionExtensions.cs b/zero.Core/Extensions/ServiceCollectionExtensions.cs index 73b65966..4a4c31bc 100644 --- a/zero.Core/Extensions/ServiceCollectionExtensions.cs +++ b/zero.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,11 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -namespace zero; +namespace zero.Extensions; public static class ServiceCollectionExtensions { diff --git a/zero.Core/Extensions/StringExtensions.cs b/zero.Core/Extensions/StringExtensions.cs index 16dba42b..937ad3c2 100644 --- a/zero.Core/Extensions/StringExtensions.cs +++ b/zero.Core/Extensions/StringExtensions.cs @@ -1,9 +1,7 @@ -using System; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text.RegularExpressions; -namespace zero; +namespace zero.Extensions; public static class StringExtensions { diff --git a/zero.Core/FileStorage/FileResult.cs b/zero.Core/FileStorage/FileResult.cs index aff5245a..d4bcc98b 100644 --- a/zero.Core/FileStorage/FileResult.cs +++ b/zero.Core/FileStorage/FileResult.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.FileStorage; public class FileResult { diff --git a/zero.Core/FileStorage/FileSizeNotation.cs b/zero.Core/FileStorage/FileSizeNotation.cs index 213020fb..d85a36e0 100644 --- a/zero.Core/FileStorage/FileSizeNotation.cs +++ b/zero.Core/FileStorage/FileSizeNotation.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.FileStorage; public enum FileSizeNotation { diff --git a/zero.Core/FileStorage/Paths.cs b/zero.Core/FileStorage/Paths.cs new file mode 100644 index 00000000..435a482c --- /dev/null +++ b/zero.Core/FileStorage/Paths.cs @@ -0,0 +1,220 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.StaticFiles; +using System.IO; +using System.Text; + +namespace zero.FileStorage; + +public class Paths : IPaths +{ + public string WebRoot { get; set; } + + public string ContentRoot { get; set; } + + public string SecureRoot { get; set; } + + public string Media { get; set; } + + protected IWebHostEnvironment Env { get; set; } + + private bool IsDebug { get; set; } + + public const string MEDIA_FOLDER = "Uploads"; + + public const char PATH_SEPARATOR = '/'; + + private static char[] InvalidFilenameChars = null; + + private const char REPLACEMENT_CHAR = '-'; + + private FileExtensionContentTypeProvider FileExtensionContentTypeProvider { get; set; } + + private static Dictionary Replacements = new Dictionary() + { + { 'ä', "ae" }, + { 'ü', "ue" }, + { 'ö', "oe" }, + { 'ß', "ss" } + }; + + + public Paths(IWebHostEnvironment env, bool isDebug) + { + Env = env; + IsDebug = isDebug; + WebRoot = env.WebRootPath; + ContentRoot = env.ContentRootPath; + SecureRoot = Path.Combine(ContentRoot, "wwwroot.secure"); + Media = Path.Combine(WebRoot, MEDIA_FOLDER); + FileExtensionContentTypeProvider = new(); + } + + + /// + /// Combine a path + /// + public string Combine(params string[] paths) + { + return Path.Combine(paths); + } + + + /// + /// Map a path + /// + public string Map(string path) + { + return Path.Combine(WebRoot, path); + } + + + /// + /// Map a secure path + /// + public string MapSecure(string path) + { + return Path.Combine(SecureRoot, path); + } + + + /// + /// Map a path + /// + public string Map(params string[] paths) + { + return Path.Combine(WebRoot, Path.Combine(paths)); + } + + + /// + /// Map a secure path + /// + public string MapSecure(params string[] paths) + { + return Path.Combine(SecureRoot, Path.Combine(paths)); + } + + + /// + /// Create a directory if it does not exist yet + /// + public void Create(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + + + /// + /// Get content type for a filename + /// + public string GetContentType(string filename, string fallback = "application/octet-stream") + { + if (filename == null || !FileExtensionContentTypeProvider.TryGetContentType(Path.GetFileName(filename), out string contentType)) + { + contentType = fallback; + } + return contentType; + } + + + /// + /// Normalizes a filename and removes invalid chars + /// + public string ToFilename(string value) + { + if (String.IsNullOrWhiteSpace(value)) + { + return value; + } + + // lowercase for string + value = value.ToLower(); + + StringBuilder sb = new StringBuilder(value.Length); + + var invalids = InvalidFilenameChars ?? (InvalidFilenameChars = Path.GetInvalidFileNameChars()); + bool changed = false; + + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + + if (Replacements.ContainsKey(c)) + { + changed = true; + sb.Append(Replacements[c]); + } + else if (invalids.Contains(c)) + { + changed = true; + sb.Append(REPLACEMENT_CHAR); + } + else + { + sb.Append(c); + } + } + + if (sb.Length == 0) + { + return "_"; + } + + return changed ? sb.ToString() : value; + } +} + + +public interface IPaths +{ + string ContentRoot { get; set; } + + string WebRoot { get; set; } + + string SecureRoot { get; set; } + + string Media { get; set; } + + /// + /// Combine a path + /// + string Combine(params string[] paths); + + /// + /// Map a path + /// + string Map(string path); + + /// + /// Map a secure path + /// + string MapSecure(string path); + + /// + /// Map a path + /// + string Map(params string[] paths); + + /// + /// Map a secure path + /// + string MapSecure(params string[] paths); + + /// + /// Create a directory if it does not exist yet + /// + void Create(string directory); + + /// + /// Get content type for a filename + /// + string GetContentType(string filename, string fallback = "application/octet-stream"); + + /// + /// Normalizes a filename and removes invalid chars + /// + string ToFilename(string value); +} \ No newline at end of file diff --git a/old/zero.Core/Api/AuthenticationApi.cs b/zero.Core/Identity/Api/AuthenticationApi.cs similarity index 100% rename from old/zero.Core/Api/AuthenticationApi.cs rename to zero.Core/Identity/Api/AuthenticationApi.cs diff --git a/old/zero.Core/Api/AuthorizationApi.cs b/zero.Core/Identity/Api/AuthorizationApi.cs similarity index 100% rename from old/zero.Core/Api/AuthorizationApi.cs rename to zero.Core/Identity/Api/AuthorizationApi.cs diff --git a/old/zero.Core/Api/PermissionsApi.cs b/zero.Core/Identity/Api/PermissionsApi.cs similarity index 100% rename from old/zero.Core/Api/PermissionsApi.cs rename to zero.Core/Identity/Api/PermissionsApi.cs diff --git a/zero.Core/Identity/AuthenticationBuilderExtensions.cs b/zero.Core/Identity/AuthenticationBuilderExtensions.cs new file mode 100644 index 00000000..18c26b93 --- /dev/null +++ b/zero.Core/Identity/AuthenticationBuilderExtensions.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace zero.Identity; + +public static class AuthenticationBuilderExtensions +{ + public static AuthenticationBuilder AddZeroBackofficeCookie(this AuthenticationBuilder builder, Action> setupAction = null) + where TUser : BackofficeUser + where TRole : BackofficeUserRole + { + return builder.AddCookie(Constants.Auth.BackofficeScheme, Constants.Auth.BackofficeDisplayName, true, b => + { + b.Configure((options, zero) => + { + options.ExpireTimeSpan = TimeSpan.FromMinutes(90); + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.Name = Constants.Auth.BackofficeCookieName; + + options.CookieManager = new ContextualCookieManager((ctx, key) => + { + return ctx.Request.Path.ToString().StartsWith(zero.BackofficePath.EnsureStartsWith('/').TrimEnd('/')); + }); + + options.Events.OnRedirectToLogin = ctx => + { + ctx.Response.StatusCode = 401; + return Task.CompletedTask; + }; + }); + + if (setupAction != null) + { + setupAction(b); + } + }); + } + + + + public static AuthenticationBuilder AddZeroCookie(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action> setupAction = null) + where TUser : ZeroIdentityUser + where TRole : ZeroIdentityRole + { + return builder.AddCookie(scheme, cookieDisplayName, false, setupAction); + } + + + + public static AuthenticationBuilder AddZeroCookie(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action> setupAction = null) + where TUser : ZeroIdentityUser + { + return builder.AddCookie(scheme, cookieDisplayName, false, setupAction); + } + + + + static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string scheme, string displayName, bool isBackoffice, Action> setupAction = null) + where TUser : ZeroIdentityUser + { + IServiceCollection services = builder.Services; + + services + .AddOptions>() + .Configure((options, zero) => + { + options.Scheme = scheme; + options.Path = isBackoffice ? zero.BackofficePath : "/"; + }); + + var optionsBuilder = services + .AddOptions(scheme) + .Configure>>((options, monitor) => + { + ZeroAuthOptions opts = monitor.Get(scheme); + + options.SlidingExpiration = true; + options.ExpireTimeSpan = TimeSpan.FromDays(90); + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.Path = "/"; + + options.LoginPath = opts.Path; + options.LogoutPath = opts.Path; + options.AccessDeniedPath = opts.Path; + }); + + if (setupAction != null) + { + setupAction(optionsBuilder); + } + + builder.AddScheme(scheme, displayName, null); + + return builder; + } +} \ No newline at end of file diff --git a/zero.Core/Identity/BackofficeUserExtensions.cs b/zero.Core/Identity/BackofficeUserExtensions.cs new file mode 100644 index 00000000..74f4d0be --- /dev/null +++ b/zero.Core/Identity/BackofficeUserExtensions.cs @@ -0,0 +1,18 @@ +namespace zero.Identity; + +public static class BackofficeUserExtensions +{ + public static string[] GetAllowedAppIds(this BackofficeUser user) + { + if (user == null) + { + return new string[0] { }; + } + + IEnumerable permissions = user.Claims + .Where(claim => claim.Type == Constants.Auth.Claims.Permission && claim.Value.StartsWith(Permissions.Applications)) + .Select(x => Permission.FromClaim(x.ToClaim(), Permissions.Applications)); + + return permissions.Where(x => x.IsTrue).Select(x => x.NormalizedKey).ToArray(); + } +} \ No newline at end of file diff --git a/zero.Core/Identity/Base32.cs b/zero.Core/Identity/Base32.cs index 2ee3d400..2abcc510 100644 --- a/zero.Core/Identity/Base32.cs +++ b/zero.Core/Identity/Base32.cs @@ -1,7 +1,6 @@ -using System; -using System.Text; +using System.Text; -namespace zero; +namespace zero.Identity; // This is a re-implementation of ASP.NET Core Base32, as they have marked it as internal // by using this we can create user security stamps on the fly and don't need diff --git a/old/zero.Core/Api/UserApi.cs b/zero.Core/Identity/Collections/UserApi.cs similarity index 100% rename from old/zero.Core/Api/UserApi.cs rename to zero.Core/Identity/Collections/UserApi.cs diff --git a/old/zero.Core/Api/UserRolesApi.cs b/zero.Core/Identity/Collections/UserRolesApi.cs similarity index 100% rename from old/zero.Core/Api/UserRolesApi.cs rename to zero.Core/Identity/Collections/UserRolesApi.cs diff --git a/zero.Core/Identity/IdentityModule.cs b/zero.Core/Identity/IdentityModule.cs index 116405cc..6b176025 100644 --- a/zero.Core/Identity/IdentityModule.cs +++ b/zero.Core/Identity/IdentityModule.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace zero; +namespace zero.Identity; internal class IdentityModule : ZeroModule { diff --git a/zero.Core/Identity/Entities/BackofficeUser.cs b/zero.Core/Identity/Models/BackofficeUser.cs similarity index 94% rename from zero.Core/Identity/Entities/BackofficeUser.cs rename to zero.Core/Identity/Models/BackofficeUser.cs index 8ec6c570..513787e3 100644 --- a/zero.Core/Identity/Entities/BackofficeUser.cs +++ b/zero.Core/Identity/Models/BackofficeUser.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; [RavenCollection("Users")] public class BackofficeUser : ZeroIdentityUser diff --git a/zero.Core/Identity/Entities/BackofficeUserRole.cs b/zero.Core/Identity/Models/BackofficeUserRole.cs similarity index 91% rename from zero.Core/Identity/Entities/BackofficeUserRole.cs rename to zero.Core/Identity/Models/BackofficeUserRole.cs index 5aaadfec..542d5551 100644 --- a/zero.Core/Identity/Entities/BackofficeUserRole.cs +++ b/zero.Core/Identity/Models/BackofficeUserRole.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; [RavenCollection("Roles")] public class BackofficeUserRole : ZeroIdentityRole diff --git a/zero.Core/Identity/Entities/UserClaim.cs b/zero.Core/Identity/Models/UserClaim.cs similarity index 94% rename from zero.Core/Identity/Entities/UserClaim.cs rename to zero.Core/Identity/Models/UserClaim.cs index 1408fb6a..523e4b76 100644 --- a/zero.Core/Identity/Entities/UserClaim.cs +++ b/zero.Core/Identity/Models/UserClaim.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Security.Claims; -namespace zero; +namespace zero.Identity; public class UserClaim { diff --git a/zero.Core/Identity/Entities/ZeroIdentityRole.cs b/zero.Core/Identity/Models/ZeroIdentityRole.cs similarity index 90% rename from zero.Core/Identity/Entities/ZeroIdentityRole.cs rename to zero.Core/Identity/Models/ZeroIdentityRole.cs index bdd6a59f..8a1cf5e8 100644 --- a/zero.Core/Identity/Entities/ZeroIdentityRole.cs +++ b/zero.Core/Identity/Models/ZeroIdentityRole.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace zero; +namespace zero.Identity; public abstract class ZeroIdentityRole : ZeroEntity { diff --git a/zero.Core/Identity/Entities/ZeroIdentityUser.cs b/zero.Core/Identity/Models/ZeroIdentityUser.cs similarity index 96% rename from zero.Core/Identity/Entities/ZeroIdentityUser.cs rename to zero.Core/Identity/Models/ZeroIdentityUser.cs index 853bb0c5..96863743 100644 --- a/zero.Core/Identity/Entities/ZeroIdentityUser.cs +++ b/zero.Core/Identity/Models/ZeroIdentityUser.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; -namespace zero; +namespace zero.Identity; public abstract class ZeroIdentityUser : ZeroEntity { diff --git a/zero.Core/Identity/Permissions/EntityPermission.cs b/zero.Core/Identity/Permissions/EntityPermission.cs index c2372ba1..0b98a3c1 100644 --- a/zero.Core/Identity/Permissions/EntityPermission.cs +++ b/zero.Core/Identity/Permissions/EntityPermission.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; public class EntityPermission { diff --git a/zero.Core/Identity/Permissions/Permission.cs b/zero.Core/Identity/Permissions/Permission.cs index 1dedb15b..5ed68801 100644 --- a/zero.Core/Identity/Permissions/Permission.cs +++ b/zero.Core/Identity/Permissions/Permission.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Security.Claims; -namespace zero; +namespace zero.Identity; public class Permission { diff --git a/zero.Core/Identity/Permissions/PermissionCollection.cs b/zero.Core/Identity/Permissions/PermissionCollection.cs index 077546f2..b68061ed 100644 --- a/zero.Core/Identity/Permissions/PermissionCollection.cs +++ b/zero.Core/Identity/Permissions/PermissionCollection.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace zero; +namespace zero.Identity; public class PermissionCollection { diff --git a/zero.Core/Identity/Permissions/PermissionGroupCollection.cs b/zero.Core/Identity/Permissions/PermissionGroupCollection.cs index f702a169..68cc728f 100644 --- a/zero.Core/Identity/Permissions/PermissionGroupCollection.cs +++ b/zero.Core/Identity/Permissions/PermissionGroupCollection.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace zero; +namespace zero.Identity; public class PermissionGroupCollection : List { diff --git a/zero.Core/Identity/Permissions/PermissionValueType.cs b/zero.Core/Identity/Permissions/PermissionValueType.cs index 07c56657..7cbb361f 100644 --- a/zero.Core/Identity/Permissions/PermissionValueType.cs +++ b/zero.Core/Identity/Permissions/PermissionValueType.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; public enum PermissionValueType { diff --git a/zero.Core/Identity/Permissions/Permissions.cs b/zero.Core/Identity/Permissions/Permissions.cs index dab925e9..9799c330 100644 --- a/zero.Core/Identity/Permissions/Permissions.cs +++ b/zero.Core/Identity/Permissions/Permissions.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; public struct Permissions { diff --git a/zero.Core/Identity/RavenRoleStore(TRole).cs b/zero.Core/Identity/RavenRoleStore(TRole).cs index d5ec01c0..38329af4 100644 --- a/zero.Core/Identity/RavenRoleStore(TRole).cs +++ b/zero.Core/Identity/RavenRoleStore(TRole).cs @@ -7,7 +7,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; public class RavenRoleStore : IRoleStore, IRoleClaimStore where TRole : ZeroIdentityRole diff --git a/zero.Core/Identity/RavenScopedStores.cs b/zero.Core/Identity/RavenScopedStores.cs index 9e573b20..982b4702 100644 --- a/zero.Core/Identity/RavenScopedStores.cs +++ b/zero.Core/Identity/RavenScopedStores.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace zero; +namespace zero.Identity; public class RavenCoreRoleStore : RavenRoleStore where TRole : ZeroIdentityRole diff --git a/zero.Core/Identity/RavenUserStore(TUser).cs b/zero.Core/Identity/RavenUserStore(TUser).cs index 751d831c..49b98e5e 100644 --- a/zero.Core/Identity/RavenUserStore(TUser).cs +++ b/zero.Core/Identity/RavenUserStore(TUser).cs @@ -2,14 +2,13 @@ using Raven.Client.Documents; using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; public partial class RavenUserStore : IUserStore, diff --git a/zero.Core/Identity/RavenUserStore(TUser,TRole).cs b/zero.Core/Identity/RavenUserStore(TUser,TRole).cs index aa32b6c0..7a1d45b3 100644 --- a/zero.Core/Identity/RavenUserStore(TUser,TRole).cs +++ b/zero.Core/Identity/RavenUserStore(TUser,TRole).cs @@ -1,13 +1,9 @@ using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; // TODO we can't inject IZeroContext here for app-context as the IApplicationContext itself // relies on UserManager and therefore this UserStore, i.e. circular dependency diff --git a/zero.Core/Identity/Security/ContextualCookieManager.cs b/zero.Core/Identity/Security/ContextualCookieManager.cs index 8ce53972..e47c3db7 100644 --- a/zero.Core/Identity/Security/ContextualCookieManager.cs +++ b/zero.Core/Identity/Security/ContextualCookieManager.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; -using System; -namespace zero; +namespace zero.Identity; public class ContextualCookieManager : ChunkingCookieManager, ICookieManager { diff --git a/zero.Core/Identity/Security/SchemedSignInManager.cs b/zero.Core/Identity/Security/SchemedSignInManager.cs index 442ebc57..775a0e89 100644 --- a/zero.Core/Identity/Security/SchemedSignInManager.cs +++ b/zero.Core/Identity/Security/SchemedSignInManager.cs @@ -3,13 +3,12 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; // TODO although the login per application works // the authentication breaks when another application signs in a user diff --git a/zero.Core/Identity/Security/ZeroAuthOptions.cs b/zero.Core/Identity/Security/ZeroAuthOptions.cs index 85d7825b..e3cd155e 100644 --- a/zero.Core/Identity/Security/ZeroAuthOptions.cs +++ b/zero.Core/Identity/Security/ZeroAuthOptions.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Identity; public class ZeroAuthOptions where TUser : ZeroIdentityUser { diff --git a/zero.Core/Identity/Security/ZeroAuthorizeAttribute.cs b/zero.Core/Identity/Security/ZeroAuthorizeAttribute.cs index 1710ca28..d2370f9f 100644 --- a/zero.Core/Identity/Security/ZeroAuthorizeAttribute.cs +++ b/zero.Core/Identity/Security/ZeroAuthorizeAttribute.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; -namespace zero; +namespace zero.Identity; public class ZeroAuthorizeAttribute : TypeFilterAttribute { diff --git a/zero.Core/Identity/Security/ZeroBackofficeClaimsPrincipalFactory.cs b/zero.Core/Identity/Security/ZeroBackofficeClaimsPrincipalFactory.cs index 78109c8e..1cb61b12 100644 --- a/zero.Core/Identity/Security/ZeroBackofficeClaimsPrincipalFactory.cs +++ b/zero.Core/Identity/Security/ZeroBackofficeClaimsPrincipalFactory.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; public class ZeroBackofficeClaimsPrincipalFactory : ZeroClaimsPrinicipalFactory where TUser : BackofficeUser diff --git a/zero.Core/Identity/Security/ZeroClaimsPrinicipalFactory.cs b/zero.Core/Identity/Security/ZeroClaimsPrinicipalFactory.cs index 5e7e5906..52a2f1d8 100644 --- a/zero.Core/Identity/Security/ZeroClaimsPrinicipalFactory.cs +++ b/zero.Core/Identity/Security/ZeroClaimsPrinicipalFactory.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; -namespace zero; +namespace zero.Identity; public class ZeroClaimsPrinicipalFactory : ZeroClaimsPrinicipalFactory where TUser : ZeroIdentityUser diff --git a/zero.Core/Identity/UserIdentity.cs b/zero.Core/Identity/UserIdentity.cs index 72b6f6d5..5cbe7adc 100644 --- a/zero.Core/Identity/UserIdentity.cs +++ b/zero.Core/Identity/UserIdentity.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Security.Claims; using System.Security.Principal; -namespace zero; +namespace zero.Identity; public class BackofficeUserIdentity : UserIdentity { diff --git a/zero.Core/Identity/Validation/BackofficeUserRoleValidator.cs b/zero.Core/Identity/Validation/BackofficeUserRoleValidator.cs new file mode 100644 index 00000000..aff1031e --- /dev/null +++ b/zero.Core/Identity/Validation/BackofficeUserRoleValidator.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using System; + +namespace zero.Identity; + +public class BackofficeUserRoleValidator : ZeroValidator +{ + const string SECTION_CLAIM = "section."; + + const string TRUE_CLAIM_VALUE = ":true"; + + public BackofficeUserRoleValidator() + { + RuleFor(x => x.Name).Length(2, 80); + RuleFor(x => x.Description).MaximumLength(200); + RuleFor(x => x.Icon).NotEmpty(); + + RuleFor(x => x.Claims).NotEmpty().Must((role, claims, context) => + { + foreach (UserClaim claim in claims) + { + if (claim.Value.StartsWith(SECTION_CLAIM, StringComparison.InvariantCultureIgnoreCase) && claim.Value.EndsWith(TRUE_CLAIM_VALUE, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return false; + }).WithMessage("@errors.role.nosection"); + + RuleForEach(x => x.Claims).Must((role, claim, context) => + { + return !claim.Type.IsNullOrEmpty() && !claim.Value.IsNullOrEmpty(); + }).WithMessage("@errors.role.emptyclaim"); + } +} diff --git a/zero.Core/Identity/Validation/BackofficeUserValidator.cs b/zero.Core/Identity/Validation/BackofficeUserValidator.cs new file mode 100644 index 00000000..5ee45f36 --- /dev/null +++ b/zero.Core/Identity/Validation/BackofficeUserValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; + +namespace zero.Identity; + +public class BackofficeUserValidator : ZeroValidator +{ + public BackofficeUserValidator(bool isCreate = false) + { + RuleFor(x => x.Name).Length(2, 80); + RuleFor(x => x.Email).NotEmpty().Email().MaximumLength(120); + RuleFor(x => x.PasswordHash).NotEmpty(); + RuleFor(x => x.LanguageId).NotEmpty(); + RuleFor(x => x.RoleIds).NotEmpty(); + + if (isCreate) + { + RuleFor(x => x.IsSuper).Equals(false); + } + } +} diff --git a/zero.Core/Identity/ZeroIdentityExtensions.cs b/zero.Core/Identity/ZeroIdentityExtensions.cs index 61195d69..8c2c29be 100644 --- a/zero.Core/Identity/ZeroIdentityExtensions.cs +++ b/zero.Core/Identity/ZeroIdentityExtensions.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using System; -namespace zero; +namespace zero.Identity; public static class ZeroIdentityExtensions { diff --git a/zero.Core/Localization/CultureResolver.cs b/zero.Core/Localization/CultureResolver.cs new file mode 100644 index 00000000..347dbd60 --- /dev/null +++ b/zero.Core/Localization/CultureResolver.cs @@ -0,0 +1,71 @@ +using FluentValidation; +using Microsoft.Extensions.Logging; +using Raven.Client.Documents; +using System.Globalization; + +namespace zero.Localization; + +public class CultureResolver : ICultureResolver +{ + protected ILogger Logger { get; private set; } + + + public CultureResolver(ILogger logger) + { + Logger = logger; + } + + + /// + public async Task Resolve(IZeroContext context) + { + // TODO this is just fake, we need to correctly resolve culture here + + if (context.IsBackofficeRequest) + { + + } + else + { + var session = context.Store.Session(); + Language language = await session.Query().FirstOrDefaultAsync(); + + if (language == null) + { + Logger.LogWarning("Could not set request culture as there is no available Language stored"); + return CultureInfo.CurrentCulture; + } + + try + { + CultureInfo culture = CultureInfo.CreateSpecificCulture(language.Code); + + if (culture.ThreeLetterISOLanguageName.IsNullOrEmpty()) + { + throw new Exception("ThreeLetterISOLanguageName is empty"); + } + + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + ValidatorOptions.Global.LanguageManager.Culture = culture; + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not create culture from Language code {code}", language.Code); + return CultureInfo.CurrentCulture; + } + } + + return CultureInfo.CurrentCulture; + } +} + + +public interface ICultureResolver +{ + /// + /// Resolves the current application from either the backoffice user (in case it is backoffice request) + /// or the domain (in case it is frontend request). + /// + Task Resolve(IZeroContext context); +} diff --git a/zero.Core/Localization/LocalizeAttribute.cs b/zero.Core/Localization/LocalizeAttribute.cs new file mode 100644 index 00000000..aa8c9de2 --- /dev/null +++ b/zero.Core/Localization/LocalizeAttribute.cs @@ -0,0 +1,12 @@ +namespace zero.Localization; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = false)] +public class LocalizeAttribute : Attribute +{ + public string Key; + + public LocalizeAttribute(string key) + { + Key = key; + } +} diff --git a/zero.Core/Localization/Localizer.cs b/zero.Core/Localization/Localizer.cs new file mode 100644 index 00000000..192c358b --- /dev/null +++ b/zero.Core/Localization/Localizer.cs @@ -0,0 +1,120 @@ +using System.Reflection; + +namespace zero.Localization; + +public class Localizer : ILocalizer +{ + protected Dictionary Cache { get; private set; } = new(); + + protected IZeroStore Store { get; private set; } + + + public Localizer(IZeroStore store) + { + Store = store; + } + + + /// + public string Text(string key) => Text(key, null); + + + /// + public string Text(string key, Dictionary tokens) + { + if (key.IsNullOrEmpty()) + { + return null; + } + + if (!Cache.TryGetValue(key, out string value)) + { + Translation translation = LoadTranslation(key); + + if (translation == null) + { + return null; + } + + value = translation.Value; + + lock (Cache) // TOOD use concurrent dictionary + { + Cache[key] = value; + } + } + + if (tokens != null) + { + value = TokenReplacement.Apply(value, tokens); + } + + return value; + } + + + /// + public string Text(T enumValue) where T : Enum => Text(enumValue, null); + + + /// + public string Text(T enumValue, Dictionary tokens) where T : Enum + { + Type type = enumValue.GetType(); + MemberInfo memInfo = type.GetMember(enumValue.ToString())[0]; + return Text(memInfo.GetCustomAttribute()?.Key, tokens); + } + + + /// + public string Maybe(string key) => Maybe(key, null); + + + /// + public string Maybe(string key, Dictionary tokens) + { + return key.IsNullOrEmpty() || !key.StartsWith("@") ? key : Text(key.Substring(1), tokens); + } + + + /// + /// Get translation from database or any other source + /// + protected virtual Translation LoadTranslation(string key) + { + return Store.Session().Query().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^ + } +} + +public interface ILocalizer +{ + /// + /// + /// + string Text(string key); + + /// + /// + /// + string Text(string key, Dictionary tokens); + + /// + /// Get a text string from a [Localize] attribute + /// + string Text(T enumValue) where T : Enum; + + /// + /// Get a text string from a [Localize] attribute + /// + string Text(T enumValue, Dictionary tokens) where T : Enum; + + /// + /// Only tries to resolve the key when it is prefixed with an @ + /// + string Maybe(string key); + + /// + /// Only tries to resolve the key when it is prefixed with an @ + /// + string Maybe(string key, Dictionary tokens); +} diff --git a/zero.Core/Localization/LocalizerExtensions.cs b/zero.Core/Localization/LocalizerExtensions.cs new file mode 100644 index 00000000..256cd417 --- /dev/null +++ b/zero.Core/Localization/LocalizerExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Html; + +namespace zero.Localization; + +public static class LocalizerExtensions +{ + public static IHtmlContent Html(this ILocalizer localizer, string key) + { + string value = localizer.Text(key); + + HtmlContentBuilder builder = new(); + builder.SetHtmlContent(value); + return builder; + } + + + public static IHtmlContent Html(this ILocalizer localizer, string key, Dictionary tokens) + { + string value = localizer.Text(key, tokens); + + HtmlContentBuilder builder = new(); + builder.SetHtmlContent(value); + return builder; + } +} \ No newline at end of file diff --git a/zero.Core/Localization/Models/Country.cs b/zero.Core/Localization/Models/Country.cs new file mode 100644 index 00000000..4323ad0a --- /dev/null +++ b/zero.Core/Localization/Models/Country.cs @@ -0,0 +1,15 @@ +namespace zero.Localization; + +[RavenCollection("Countries")] +public class Country : ZeroEntity +{ + /// + /// Preferred countries are displayed on top in lists + /// + public bool IsPreferred { get; set; } + + /// + /// Country code (ISO 3166-1) + /// + public string Code { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Localization/Models/Culture.cs b/zero.Core/Localization/Models/Culture.cs new file mode 100644 index 00000000..eed5ff88 --- /dev/null +++ b/zero.Core/Localization/Models/Culture.cs @@ -0,0 +1,8 @@ +namespace zero.Localization; + +public class Culture +{ + public string Code { get; set; } + + public string Name { get; set; } +} diff --git a/zero.Core/Localization/Models/Language.cs b/zero.Core/Localization/Models/Language.cs new file mode 100644 index 00000000..4dffc7d8 --- /dev/null +++ b/zero.Core/Localization/Models/Language.cs @@ -0,0 +1,25 @@ +namespace zero.Localization; + +[RavenCollection("Languages")] +public class Language : ZeroEntity +{ + /// + /// Language code (ISO 3166-1) + /// + public string Code { get; set; } + + /// + /// Whether this is the default language + /// + public bool IsDefault { get; set; } + + /// + /// Whether this language is optional and does not have to be filled out + /// + public bool IsOptional { get; set; } + + /// + /// If this language is inherited it gets all missing properties from its parent + /// + public string InheritedLanguageId { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Localization/Models/Translation.cs b/zero.Core/Localization/Models/Translation.cs new file mode 100644 index 00000000..91febce3 --- /dev/null +++ b/zero.Core/Localization/Models/Translation.cs @@ -0,0 +1,27 @@ +namespace zero.Localization; + +[RavenCollection("Translations")] +public class Translation : ZeroEntity +{ + public Translation() + { + IsActive = true; + } + + /// + /// Value of the translation + /// + public string Value { get; set; } + + /// + /// Display + input type + /// + public TranslationDisplay Display { get; set; } +} + + +public enum TranslationDisplay +{ + Text = 0, + HTML = 1 +} \ No newline at end of file diff --git a/zero.Core/Localization/Module.cs b/zero.Core/Localization/Module.cs new file mode 100644 index 00000000..67f4a7d8 --- /dev/null +++ b/zero.Core/Localization/Module.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Localization; + +internal class LocalizationModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Mails/FileMailDispatcher.cs b/zero.Core/Mails/FileMailDispatcher.cs new file mode 100644 index 00000000..b7843b9c --- /dev/null +++ b/zero.Core/Mails/FileMailDispatcher.cs @@ -0,0 +1,89 @@ +using Newtonsoft.Json; +using System.IO; +using System.Text; + +namespace zero.Mails; + +/// +/// Default implementation of an IMailSender which sends the mail a flat file +/// and therefore not using the SMTP channel. +/// Implementing real mail sending is up to the consuming application. +/// +public class FileMailDispatcher : IMailDispatcher +{ + protected Queue Queue { get; private set; } = new Queue(); + + protected IPaths PathResolver { get; private set; } + + string MailDirectory; + + + public FileMailDispatcher(IPaths pathResolver) + { + PathResolver = pathResolver; + MailDirectory = "mails"; + } + + + /// + public void Enqueue(Mail message) + { + Queue.Enqueue(message); + } + + + /// + public async Task Send(CancellationToken token = default) + { + if (Queue.Count < 1) + { + return; + } + + string folder = PathResolver.Map(MailDirectory); + PathResolver.Create(folder); + + while (Queue.Count > 0) + { + Mail message = Queue.Dequeue(); + string content = JsonConvert.SerializeObject(message, new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.None, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); + + string filename = Safenames.File(DateTimeOffset.Now.ToString("yyyy-MM-dd-HH-mm-ss") + "_" + message.To[0].Address + ".txt"); + string path = PathResolver.Map(folder, filename); + + await File.WriteAllTextAsync(path, content, token); + } + } + + + /// + public void Dispose() { } + + + /// + /// Creats the file content from a mail message + /// + string CreateContent(Mail message) + { + StringBuilder text = new(); + text.AppendLine("To: " + message.To); + text.AppendLine("CC: " + message.CC); + text.AppendLine("Bcc: " + message.Bcc); + text.AppendLine("From: " + message.From); + text.AppendLine("Subject: " + message.Subject); + text.Append("Date: " + DateTimeOffset.UtcNow.ToString()); + + text.AppendLine(); + text.AppendLine("=============== BODY ==============="); + text.AppendLine(); + + text.AppendLine(message.Body); + + return text.ToString(); + } +} diff --git a/zero.Core/Mails/IMailDispatcher.cs b/zero.Core/Mails/IMailDispatcher.cs new file mode 100644 index 00000000..4f1d95d1 --- /dev/null +++ b/zero.Core/Mails/IMailDispatcher.cs @@ -0,0 +1,19 @@ +namespace zero.Mails; + +public interface IMailDispatcher : IDisposable +{ + /// + /// Adds a new mail message to the outgoing queue + /// + void Enqueue(Mail message); + + /// + /// Sends all mails which have been added to the queue previously + /// + Task Send(CancellationToken token = default); + + /// + /// Whether a certain sender signature is supported by this dispatcher + /// + Task IsSenderSupported(string email) => Task.FromResult(true); +} \ No newline at end of file diff --git a/zero.Core/Mails/LoggerMailDispatcher.cs b/zero.Core/Mails/LoggerMailDispatcher.cs new file mode 100644 index 00000000..9fb4f5e2 --- /dev/null +++ b/zero.Core/Mails/LoggerMailDispatcher.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; + +namespace zero.Mails; + +/// +/// Default implementation of an IMailSender which sends the mail to the attached logger +/// and therefore not using the SMTP channel. +/// Implementing real mail sending is up to the consuming application. +/// +public class LoggerMailDispatcher : IMailDispatcher +{ + protected Queue Queue { get; private set; } = new Queue(); + + protected ILogger Logger { get; set; } + + + public LoggerMailDispatcher(ILogger logger) + { + Logger = logger; + } + + + /// + public void Enqueue(Mail message) + { + Queue.Enqueue(message); + } + + + /// + public Task Send(CancellationToken token = default) + { + while (Queue.Count > 0) + { + Mail message = Queue.Dequeue(); + Logger.LogInformation("Mail to {to}. Subject: {subject}", message.To[0].Address, message.Subject); + } + + return Task.CompletedTask; + } + + + /// + public void Dispose() { } +} \ No newline at end of file diff --git a/zero.Core/Mails/Mail.cs b/zero.Core/Mails/Mail.cs new file mode 100644 index 00000000..1f5c6bcf --- /dev/null +++ b/zero.Core/Mails/Mail.cs @@ -0,0 +1,27 @@ +using System.Net.Mail; + +namespace zero.Mails; + +public class Mail : Mail where T : class +{ + public T Model { get; set; } +} + +public class Mail : MailMessage +{ + public bool IsDeactivated { get; set; } + + public string ViewPath { get; set; } + + public bool HasView { get; set; } = true; + + public bool IsRendered { get; set; } + + public string Preheader { get; set; } + + public MailTemplate Template { get; set; } + + public MailPlaceholders Placeholders { get; set; } = new(); + + public MailMetadata Metadata { get; set; } = new(); +} \ No newline at end of file diff --git a/zero.Core/Mails/MailMetadata.cs b/zero.Core/Mails/MailMetadata.cs new file mode 100644 index 00000000..2b6d227e --- /dev/null +++ b/zero.Core/Mails/MailMetadata.cs @@ -0,0 +1,6 @@ +namespace zero.Mails; + +public class MailMetadata : Dictionary +{ + +} diff --git a/zero.Core/Mails/MailPlaceholders.cs b/zero.Core/Mails/MailPlaceholders.cs new file mode 100644 index 00000000..799f661e --- /dev/null +++ b/zero.Core/Mails/MailPlaceholders.cs @@ -0,0 +1,6 @@ +namespace zero.Mails; + +public class MailPlaceholders : Dictionary +{ + +} \ No newline at end of file diff --git a/zero.Core/Mails/MailProvider.cs b/zero.Core/Mails/MailProvider.cs new file mode 100644 index 00000000..8fcfd977 --- /dev/null +++ b/zero.Core/Mails/MailProvider.cs @@ -0,0 +1,201 @@ +using Microsoft.Extensions.Logging; +using System.Net.Mail; +using System.Text; +using System.Text.RegularExpressions; + +namespace zero.Mails; + +public class MailProvider : IMailProvider +{ + protected IMailTemplatesCollection Collection { get; set; } + + protected ILogger Logger { get; set; } + + protected IZeroContext Zero { get; set; } + + protected IMailDispatcher MailSender { get; set; } + + protected IRazorRenderer Renderer { get; set; } + + protected Regex PlaceholderRegex { get; set; } + + private Encoding encoding = Encoding.UTF8; + + + public MailProvider(IZeroContext zero, IMailTemplatesCollection collection, ILogger logger, IMailDispatcher mailSender, IRazorRenderer renderer) + { + Zero = zero; + Collection = collection; + Logger = logger; + MailSender = mailSender; + Renderer = renderer; + PlaceholderRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase); + } + + + /// + public virtual async Task Create(string mailTemplateKey, Action onCreate = null) where T : Mail, new() + { + MailTemplate template = await GetMailTemplate(mailTemplateKey); + + if (template == null) + { + Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey); + return null; + } + + onCreate?.Invoke(template); + + return Merge(new T(), template); + } + + + /// + public virtual async Task Create(string mailTemplateKey, Action onCreate = null) + { + MailTemplate template = await GetMailTemplate(mailTemplateKey); + + if (template == null) + { + Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey); + return null; + } + + onCreate?.Invoke(template); + + return Merge(new Mail(), template); + } + + + /// + public virtual async Task Send(Mail message, CancellationToken token = default) + { + await Send(message, MailSender, token); + } + + + /// + public virtual async Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default) + { + if (message.IsDeactivated) + { + return; + } + + try + { + await Render(message); + dispatcher.Enqueue(message); + await dispatcher.Send(token); + Logger.LogInformation("Dispatched email (template: {template}) to {recipient}", message.Template?.Alias ?? "none", message.To); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to send mail message with template @{template}", message.Template.Key); + } + } + + + + protected virtual async Task GetMailTemplate(string key) + { + return await Collection.GetByKey(key); + } + + + /// + public virtual async Task Render(Mail message) + { + message.Subject = TokenReplacement.Apply(message.Subject, message.Placeholders); + message.Body = TokenReplacement.Apply(message.Body, message.Placeholders); + message.Preheader = TokenReplacement.Apply(message.Preheader, message.Placeholders); + + if (!message.HasView) + { + message.IsRendered = true; + return message.Body; + } + + string viewPath = message.ViewPath; + + if (viewPath.IsNullOrEmpty()) + { + viewPath = $"~/Views/Mails/{message.Template.Key.Replace('.', '/')}.cshtml"; + } + + message.Body = await Renderer.ViewAsync(viewPath, message); + message.IsBodyHtml = true; + message.IsRendered = true; + return message.Body; + } + + + protected virtual T Merge(T mail, MailTemplate template) where T : Mail + { + mail.Template = template; + mail.IsDeactivated = !mail.Template.IsActive; + + // get sender from template or fall back to application + mail.From = new MailAddress(template.SenderEmail.Or(Zero.Application.Email), template.SenderName.Or(Zero.Application.FullName), encoding); + mail.Sender = mail.From; + mail.ReplyToList.Add(mail.From); + + // cc + bcc (multiple addresses are separated with commas + if (!template.Cc.IsNullOrWhiteSpace()) + { + mail.CC.Add(template.Cc); + } + if (!template.Bcc.IsNullOrWhiteSpace()) + { + mail.Bcc.Add(template.Bcc); + } + + // recipient + if (!template.RecipientEmail.IsNullOrWhiteSpace()) + { + mail.To.Add(template.RecipientEmail); + } + + // subject + mail.Subject = template.Subject; + mail.SubjectEncoding = encoding; + + // body + mail.Body = mail.Template.Body; + mail.IsBodyHtml = true; + mail.BodyEncoding = encoding; + mail.Preheader = mail.Template.Preheader; + + return mail; + } +} + + +public interface IMailProvider +{ + /// + /// Creates a maling from a template + /// + Task Create(string mailTemplateKey, Action onCreate = null); + + /// + /// Creates a maling from a template + /// + Task Create(string mailTemplateKey, Action onCreate = null) where T : Mail, new(); + + /// + /// Renders the message body. + /// This is automatically called when sending messages. + /// + Task Render(Mail message); + + /// + /// Sends a message with the default dispatcher + /// + Task Send(Mail message, CancellationToken token = default); + + /// + /// Sends a message with the specified dispatcher + /// + Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default); +} diff --git a/zero.Core/Mails/MailSendResult.cs b/zero.Core/Mails/MailSendResult.cs new file mode 100644 index 00000000..ede3721d --- /dev/null +++ b/zero.Core/Mails/MailSendResult.cs @@ -0,0 +1,10 @@ +namespace zero.Mails; + +public class MailSendResult +{ + public string Alias { get; set; } + + public DateTimeOffset LastRunDate { get; set; } + + public MailSendResult() { } +} diff --git a/zero.Core/Mails/Models/MailTemplate.cs b/zero.Core/Mails/Models/MailTemplate.cs new file mode 100644 index 00000000..2862cd33 --- /dev/null +++ b/zero.Core/Mails/Models/MailTemplate.cs @@ -0,0 +1,45 @@ +namespace zero.Mails; + +[RavenCollection("MailTemplates")] +public class MailTemplate : ZeroEntity +{ + /// + /// Email address of the sender (overrides email from application) + /// + public string SenderEmail { get; set; } + + /// + /// Name of the sender (overrides name from application) + /// + public string SenderName { get; set; } + + /// + /// Email address of the recipient. This is only necessary for templates which do not have a dynamic recipient (e.g. reports). + /// + public string RecipientEmail { get; set; } + + /// + /// Additional comma-separated emails to send a copy to + /// + public string Cc { get; set; } + + /// + /// Additional comma-separated emails to send a hidden copy to + /// + public string Bcc { get; set; } + + /// + /// Email subject (can contain placeholders) + /// + public string Subject { get; set; } + + /// + /// Email body (can contain placeholders) + /// + public string Body { get; set; } + + /// + /// Preheader which is displayed in the preview pane (can contain placeholders) + /// + public string Preheader { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Mails/Module.cs b/zero.Core/Mails/Module.cs new file mode 100644 index 00000000..a2c1d398 --- /dev/null +++ b/zero.Core/Mails/Module.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mails; + +internal class MailsModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + config.Services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Media/Indexes/MediaFolder_ByHierarchy.cs b/zero.Core/Media/Indexes/MediaFolder_ByHierarchy.cs new file mode 100644 index 00000000..39cb641f --- /dev/null +++ b/zero.Core/Media/Indexes/MediaFolder_ByHierarchy.cs @@ -0,0 +1,43 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Media; + +public class MediaFolder_ByHierarchy : ZeroIndex +{ + public class Result : ZeroIdEntity, IZeroDbConventions + { + public string Name { get; set; } + + public List Path { get; set; } = new List(); + } + + + public class PathResult + { + public string Id { get; set; } + + public string Name { get; set; } + } + + + protected override void Create() + { + Map = items => items.Select(item => new Result + { + Id = item.Id, + Name = item.Name, + Path = Recurse(item, x => LoadDocument(x.ParentId)) + .Where(x => x != null && x.Id != null && x.Id != item.Id) + .Reverse() + .Select(current => new PathResult() + { + Id = current.Id, + Name = current.Name + }) + .ToList() + }); + + StoreAllFields(FieldStorage.Yes); + //Index(x => x.ChannelId, FieldIndexing.Exact); + } +} \ No newline at end of file diff --git a/zero.Core/Media/Indexes/MediaFolders_WithChildren.cs b/zero.Core/Media/Indexes/MediaFolders_WithChildren.cs new file mode 100644 index 00000000..62fd4b2d --- /dev/null +++ b/zero.Core/Media/Indexes/MediaFolders_WithChildren.cs @@ -0,0 +1,38 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Media; + +public class MediaFolders_WithChildren : ZeroIndex +{ + public class Result : ZeroIdEntity + { + public string ParentId { get; set; } + + public string Name { get; set; } + + public string[] ChildrenIds { get; set; } + } + + + protected override void Create() + { + Map = items => items.Where(x => x.ParentId != null).Select(item => new Result + { + Id = item.Id, + ParentId = item.ParentId, + Name = item.Name, + ChildrenIds = new string[] { } + }); + + Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result() + { + Id = group.Key.ParentId, + ParentId = group.Key.ParentId, + Name = String.Empty, + ChildrenIds = group.Select(x => x.Id).ToArray() + }); + + StoreAllFields(FieldStorage.Yes); + //Index(x => x.ChannelId, FieldIndexing.Exact); + } +} \ No newline at end of file diff --git a/zero.Core/Media/Indexes/Media_ByChildren.cs b/zero.Core/Media/Indexes/Media_ByChildren.cs new file mode 100644 index 00000000..814cd661 --- /dev/null +++ b/zero.Core/Media/Indexes/Media_ByChildren.cs @@ -0,0 +1,46 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Media; + +public class Media_ByChildren : ZeroMultiMapIndex +{ + public class Result : ZeroIdEntity, IZeroDbConventions + { + public string ParentId { get; set; } + + public int ChildrenCount { get; set; } + + public string[] ChildrenIds { get; set; } + } + + + protected override void Create() + { + AddMap(items => items.Select(item => new Result() + { + Id = item.Id, + ParentId = item.FolderId, + ChildrenCount = 1, + ChildrenIds = new string[] { } + })); + + AddMap(items => items.Select(item => new Result() + { + Id = item.Id, + ParentId = item.ParentId, + ChildrenCount = 1, + ChildrenIds = new string[] { } + })); + + Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result() + { + Id = null, + ParentId = group.Key.ParentId, + ChildrenCount = group.Sum(x => x.ChildrenCount), + ChildrenIds = group.Select(x => x.Id).ToArray() + }); + + StoreAllFields(FieldStorage.Yes); + Index(x => x.ParentId, FieldIndexing.Exact); + } +} \ No newline at end of file diff --git a/zero.Core/Media/Indexes/Media_ByParent.cs b/zero.Core/Media/Indexes/Media_ByParent.cs new file mode 100644 index 00000000..a0fe06e5 --- /dev/null +++ b/zero.Core/Media/Indexes/Media_ByParent.cs @@ -0,0 +1,40 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Media; + +public class Media_ByParent : ZeroMultiMapIndex +{ + protected override void Create() + { + AddMap(items => items.Select(item => new MediaListItem + { + Id = item.Id, + ParentId = item.ParentId, + CreatedDate = item.CreatedDate, + IsFolder = true, + Name = item.Name, + Image = null, + Children = 0, + Size = 0, + IsShared = item.Blueprint != null, + AspectRatio = 0 + })); + + AddMap(items => items.Select(item => new MediaListItem + { + Id = item.Id, + ParentId = item.FolderId, + CreatedDate = item.CreatedDate, + IsFolder = false, + Name = item.Name, + Image = item.PreviewSource, + Children = 0, + Size = item.Size, + IsShared = item.Blueprint != null, + AspectRatio = item.ImageMeta != null ? (float)item.ImageMeta.Width / item.ImageMeta.Height : 0 + })); + + StoreAllFields(FieldStorage.Yes); + Index(x => x.ParentId, FieldIndexing.Exact); + } +} \ No newline at end of file diff --git a/zero.Core/Media/Models/Media.cs b/zero.Core/Media/Models/Media.cs new file mode 100644 index 00000000..aca12a48 --- /dev/null +++ b/zero.Core/Media/Models/Media.cs @@ -0,0 +1,63 @@ +namespace zero.Media; + +/// +/// A media file (can contain an image or other media like videos and documents) +/// +[RavenCollection("Media")] +public class Media : ZeroEntity +{ + /// + /// Id/name of the phyiscal folder which is stored on disk/cloud + /// + public string FileId { get; set; } + + /// + /// Id of the media folder + /// + public string FolderId { get; set; } + + /// + /// Alternative text which is used when the image can't be loaded + /// + public string AlternativeText { get; set; } + + /// + /// Additional caption text + /// + public string Caption { get; set; } + + /// + /// Path of the media item + /// + public string Source { get; set; } + + /// + /// For images this is the source for a 100x100px thumbnail + /// + public string ThumbnailSource { get; set; } + + /// + /// For images this is the source for a [proportional]x210px thumbnail + /// + public string PreviewSource { get; set; } + + /// + /// Filesize in bytes + /// + public long Size { get; set; } + + /// + /// Meta data for images + /// + public MediaImageMeta ImageMeta { get; set; } + + /// + /// Optional focal point for an image + /// + public MediaFocalPoint FocalPoint { get; set; } + + /// + /// Type of the media + /// + public MediaType Type { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Media/Models/MediaFocalPoint.cs b/zero.Core/Media/Models/MediaFocalPoint.cs new file mode 100644 index 00000000..68645a29 --- /dev/null +++ b/zero.Core/Media/Models/MediaFocalPoint.cs @@ -0,0 +1,11 @@ +namespace zero.Media; + +/// +/// The focal point sets the point of interest in an image with x/y coordinates from 0-1 +/// +public class MediaFocalPoint +{ + public decimal Left { get; set; } + + public decimal Top { get; set; } +} diff --git a/zero.Core/Media/Models/MediaFolder.cs b/zero.Core/Media/Models/MediaFolder.cs new file mode 100644 index 00000000..f5390412 --- /dev/null +++ b/zero.Core/Media/Models/MediaFolder.cs @@ -0,0 +1,13 @@ +namespace zero.Media; + +/// +/// A media folder contains media and other folders +/// +[RavenCollection("MediaFolders")] +public class MediaFolder : ZeroEntity +{ + /// + /// Parent folder id + /// + public string ParentId { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Media/Models/MediaImageMeta.cs b/zero.Core/Media/Models/MediaImageMeta.cs new file mode 100644 index 00000000..2aca31d0 --- /dev/null +++ b/zero.Core/Media/Models/MediaImageMeta.cs @@ -0,0 +1,42 @@ +namespace zero.Media; + +/// +/// Metadata for images +/// +public class MediaImageMeta +{ + /// + /// Width in pixels + /// + public int Width { get; set; } + + /// + /// Height in pixels + /// + public int Height { get; set; } + + /// + /// Resolution factor + /// + public double DPI { get; set; } + + /// + /// Date the image was taken + /// + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Original color space of the image + /// + public string ColorSpace { get; set; } + + /// + /// Whether this image contains transparent pixels + /// + public bool HasTransparency { get; set; } + + /// + /// How many frames contains this image (for animation) + /// + public int Frames { get; set; } = 1; +} \ No newline at end of file diff --git a/zero.Core/Media/Models/MediaListItem.cs b/zero.Core/Media/Models/MediaListItem.cs new file mode 100644 index 00000000..6d7d92cc --- /dev/null +++ b/zero.Core/Media/Models/MediaListItem.cs @@ -0,0 +1,24 @@ +namespace zero.Media; + +public class MediaListItem : ZeroIdEntity +{ + public string ParentId { get; set; } + + public string Name { get; set; } + + public DateTimeOffset CreatedDate { get; set; } + + public bool IsFolder { get; set; } + + public string Image { get; set; } + + public long Size { get; set; } + + public int Children { get; set; } + + public bool HasTransparency { get; set; } + + public float AspectRatio { get; set; } + + public bool IsShared { get; set; } +} diff --git a/zero.Core/Media/Models/MediaListQuery.cs b/zero.Core/Media/Models/MediaListQuery.cs new file mode 100644 index 00000000..9dea5458 --- /dev/null +++ b/zero.Core/Media/Models/MediaListQuery.cs @@ -0,0 +1,14 @@ +namespace zero.Media; + +public class MediaListQuery : ListQuery +{ + public string FolderId { get; set; } +} + + +public class MediaListItemQuery : ListQuery +{ + public string FolderId { get; set; } + + public bool SearchIsGlobal { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Media/Models/MediaSourceSize.cs b/zero.Core/Media/Models/MediaSourceSize.cs new file mode 100644 index 00000000..3fe01731 --- /dev/null +++ b/zero.Core/Media/Models/MediaSourceSize.cs @@ -0,0 +1,8 @@ +namespace zero.Media; + +public enum MediaSourceSize +{ + Original = 0, + Preview = 1, + Thumbnail = 2 +} diff --git a/zero.Core/Media/Models/MediaType.cs b/zero.Core/Media/Models/MediaType.cs new file mode 100644 index 00000000..92884f4c --- /dev/null +++ b/zero.Core/Media/Models/MediaType.cs @@ -0,0 +1,7 @@ +namespace zero.Media; + +public enum MediaType +{ + File = 0, + Image = 1 +} \ No newline at end of file diff --git a/zero.Core/Media/Models/Video.cs b/zero.Core/Media/Models/Video.cs new file mode 100644 index 00000000..648c2f41 --- /dev/null +++ b/zero.Core/Media/Models/Video.cs @@ -0,0 +1,24 @@ +namespace zero.Media; + +public class Video +{ + public VideoProvider Provider { get; set; } + + public string VideoId { get; set; } + + public string VideoUrl { get; set; } + + public string VideoPreviewImageUrl { get; set; } + + public string Title { get; set; } + + public string PreviewImageId { get; set; } +} + + +public enum VideoProvider +{ + Html, + Youtube, + Vimeo +} diff --git a/zero.Core/Pages/Entities/Page.cs b/zero.Core/Pages/Entities/Page.cs index a2ef39ea..b9d73012 100644 --- a/zero.Core/Pages/Entities/Page.cs +++ b/zero.Core/Pages/Entities/Page.cs @@ -1,6 +1,4 @@ -using System; - -namespace zero; +namespace zero.Pages; /// /// A page can consist of unlimited properties and be rendered as you wish diff --git a/zero.Core/Pages/Entities/PageFolder.cs b/zero.Core/Pages/Entities/PageFolder.cs index 5ee8d423..8f45ace2 100644 --- a/zero.Core/Pages/Entities/PageFolder.cs +++ b/zero.Core/Pages/Entities/PageFolder.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Pages; /// /// A page folder is used to group pages together but should not contain any content diff --git a/zero.Core/Pages/Entities/PageType.cs b/zero.Core/Pages/Entities/PageType.cs index f6f4f9d5..23c991d0 100644 --- a/zero.Core/Pages/Entities/PageType.cs +++ b/zero.Core/Pages/Entities/PageType.cs @@ -1,6 +1,4 @@ -using System; - -namespace zero; +namespace zero.Pages; /// /// A page type holds information about a Page implementation diff --git a/zero.Core/Pages/IPageTypeHandler.cs b/zero.Core/Pages/IPageTypeHandler.cs new file mode 100644 index 00000000..7ed5a8a7 --- /dev/null +++ b/zero.Core/Pages/IPageTypeHandler.cs @@ -0,0 +1,6 @@ +namespace zero.Pages; + +public interface IPageTypeHandler : IHandler +{ + Task> GetAllowedPageTypes(Application application, IEnumerable registeredTypes, IEnumerable parents); +} diff --git a/zero.Core/Pages/Indexes/Pages_AsHistory.cs b/zero.Core/Pages/Indexes/Pages_AsHistory.cs index a8616601..797e8e18 100644 --- a/zero.Core/Pages/Indexes/Pages_AsHistory.cs +++ b/zero.Core/Pages/Indexes/Pages_AsHistory.cs @@ -1,8 +1,6 @@ using Raven.Client.Documents.Indexes; -using System; -using System.Linq; -namespace zero; +namespace zero.Pages; public class Pages_AsHistory : ZeroIndex { diff --git a/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs b/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs index e38fd499..c6f1ff50 100644 --- a/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs +++ b/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs @@ -1,9 +1,6 @@ using Raven.Client.Documents.Indexes; -using System; -using System.Collections.Generic; -using System.Linq; -namespace zero; +namespace zero.Pages; public class Pages_ByHierarchy : ZeroIndex { diff --git a/zero.Core/Pages/Indexes/Pages_ByType.cs b/zero.Core/Pages/Indexes/Pages_ByType.cs index af2de0e3..fa6db8c7 100644 --- a/zero.Core/Pages/Indexes/Pages_ByType.cs +++ b/zero.Core/Pages/Indexes/Pages_ByType.cs @@ -1,7 +1,6 @@ using Raven.Client.Documents.Indexes; -using System.Linq; -namespace zero; +namespace zero.Pages; public class Pages_ByType : ZeroIndex { diff --git a/zero.Core/Pages/Indexes/Pages_WithChildren.cs b/zero.Core/Pages/Indexes/Pages_WithChildren.cs index 47c8b0bb..4010c4b7 100644 --- a/zero.Core/Pages/Indexes/Pages_WithChildren.cs +++ b/zero.Core/Pages/Indexes/Pages_WithChildren.cs @@ -1,8 +1,6 @@ using Raven.Client.Documents.Indexes; -using System; -using System.Linq; -namespace zero; +namespace zero.Pages; public class Pages_WithChildren : ZeroIndex { diff --git a/zero.Core/Pages/PageValidator.cs b/zero.Core/Pages/PageValidator.cs index 9fb9134c..78d49c72 100644 --- a/zero.Core/Pages/PageValidator.cs +++ b/zero.Core/Pages/PageValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -namespace zero; +namespace zero.Pages; public class PageValidator : ZeroValidator { diff --git a/zero.Core/Pages/PagesModule.cs b/zero.Core/Pages/PagesModule.cs index 5494e948..8ea5dfd6 100644 --- a/zero.Core/Pages/PagesModule.cs +++ b/zero.Core/Pages/PagesModule.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Pages; internal class PagesModule : ZeroModule { diff --git a/zero.Core/Persistence/GenerateIdAttribute.cs b/zero.Core/Persistence/GenerateIdAttribute.cs new file mode 100644 index 00000000..8318aa43 --- /dev/null +++ b/zero.Core/Persistence/GenerateIdAttribute.cs @@ -0,0 +1,17 @@ +namespace zero.Persistence; + +/// +/// Automatically generate ID with the specified length and insert it into this property on entity save +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class GenerateIdAttribute : Attribute +{ + public int? Length = null; + + public GenerateIdAttribute(int length) + { + Length = length; + } + + public GenerateIdAttribute() { } +} diff --git a/zero.Core/Persistence/IZeroDbConventions.cs b/zero.Core/Persistence/IZeroDbConventions.cs index 3b8650f1..d0433123 100644 --- a/zero.Core/Persistence/IZeroDbConventions.cs +++ b/zero.Core/Persistence/IZeroDbConventions.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Persistence; /// /// Triggers custom Raven conventions for database operations diff --git a/zero.Core/Persistence/IdGenerator.cs b/zero.Core/Persistence/IdGenerator.cs index 759572a5..ea1b7aa8 100644 --- a/zero.Core/Persistence/IdGenerator.cs +++ b/zero.Core/Persistence/IdGenerator.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; - -namespace zero; +namespace zero.Persistence; public class IdGenerator { diff --git a/zero.Core/Persistence/PersistenceModule.cs b/zero.Core/Persistence/Module.cs similarity index 98% rename from zero.Core/Persistence/PersistenceModule.cs rename to zero.Core/Persistence/Module.cs index c16fd888..584a4820 100644 --- a/zero.Core/Persistence/PersistenceModule.cs +++ b/zero.Core/Persistence/Module.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Raven.Client.Documents; using Raven.Client.Http; -using System; -namespace zero; +namespace zero.Persistence; internal class PersistenceModule : ZeroModule { diff --git a/zero.Core/Persistence/RavenCollectionAttribute.cs b/zero.Core/Persistence/RavenCollectionAttribute.cs index 3fd83500..d45d9701 100644 --- a/zero.Core/Persistence/RavenCollectionAttribute.cs +++ b/zero.Core/Persistence/RavenCollectionAttribute.cs @@ -1,6 +1,4 @@ -using System; - -namespace zero; +namespace zero.Persistence; /// /// This attribute will allow the usage of custom collection names for Raven collections diff --git a/zero.Core/Persistence/RavenOptions.cs b/zero.Core/Persistence/RavenOptions.cs index 5ea0790c..a45909a2 100644 --- a/zero.Core/Persistence/RavenOptions.cs +++ b/zero.Core/Persistence/RavenOptions.cs @@ -1,10 +1,7 @@ using Raven.Client.Documents; -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -namespace zero; +namespace zero.Persistence; public class RavenOptions { diff --git a/zero.Core/Persistence/Safenames.cs b/zero.Core/Persistence/Safenames.cs index 59bca671..6821692a 100644 --- a/zero.Core/Persistence/Safenames.cs +++ b/zero.Core/Persistence/Safenames.cs @@ -1,8 +1,7 @@ -using System; -using System.IO; +using System.IO; using System.Text; -namespace zero; +namespace zero.Persistence; public class Safenames { diff --git a/zero.Core/Persistence/Tokens/Rfc6238AuthenticationService.cs b/zero.Core/Persistence/Tokens/Rfc6238AuthenticationService.cs index 5c444884..b074c754 100644 --- a/zero.Core/Persistence/Tokens/Rfc6238AuthenticationService.cs +++ b/zero.Core/Persistence/Tokens/Rfc6238AuthenticationService.cs @@ -1,10 +1,9 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Net; using System.Security.Cryptography; using System.Text; -namespace zero; +namespace zero.Persistence; /// /// Replicates the Rfc6238AuthenticationService from ASP.NET Core internals as it isn't part of the public API diff --git a/zero.Core/Persistence/Tokens/Token.cs b/zero.Core/Persistence/Tokens/Token.cs index ba9cb0a0..95a869c8 100644 --- a/zero.Core/Persistence/Tokens/Token.cs +++ b/zero.Core/Persistence/Tokens/Token.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace zero; +namespace zero.Persistence; [RavenCollection("Tokens")] public class SecurityToken : IZeroDbConventions diff --git a/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs b/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs index e5c77515..bee55ee6 100644 --- a/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs +++ b/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs @@ -1,12 +1,9 @@ using Microsoft.AspNetCore.Cryptography.KeyDerivation; using Raven.Client.Documents.Session; -using System; -using System.Collections.Generic; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; -namespace zero; +namespace zero.Persistence; public class ZeroTokenProvider : IZeroTokenProvider { diff --git a/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs b/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs index a600d42c..ffcc3da4 100644 --- a/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs +++ b/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs @@ -1,13 +1,9 @@ using Raven.Client.Documents.Conventions; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text; -using System.Threading.Tasks; -namespace zero; +namespace zero.Persistence; public class ZeroDocumentConventionsBuilder : IZeroDocumentConventionsBuilder { diff --git a/zero.Core/Persistence/ZeroDocumentSession.cs b/zero.Core/Persistence/ZeroDocumentSession.cs index b5c0b31c..19ffc8c3 100644 --- a/zero.Core/Persistence/ZeroDocumentSession.cs +++ b/zero.Core/Persistence/ZeroDocumentSession.cs @@ -1,8 +1,7 @@ using Raven.Client.Documents; using Raven.Client.Documents.Session; -using System; -namespace zero; +namespace zero.Persistence; public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession { diff --git a/zero.Core/Persistence/ZeroDocumentStore.cs b/zero.Core/Persistence/ZeroDocumentStore.cs index 3e537406..57ad2edd 100644 --- a/zero.Core/Persistence/ZeroDocumentStore.cs +++ b/zero.Core/Persistence/ZeroDocumentStore.cs @@ -4,12 +4,8 @@ using Raven.Client.Documents.BulkInsert; using Raven.Client.Documents.Operations; using Raven.Client.Documents.Queries; using Raven.Client.Documents.Session; -using System; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Options; -namespace zero; +namespace zero.Persistence; public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore { diff --git a/zero.Core/Persistence/ZeroIndex.cs b/zero.Core/Persistence/ZeroIndex.cs index 2cc322bd..ad7927ef 100644 --- a/zero.Core/Persistence/ZeroIndex.cs +++ b/zero.Core/Persistence/ZeroIndex.cs @@ -2,13 +2,9 @@ using Raven.Client.Documents.Indexes; using Raven.Client.Documents.Indexes.Spatial; using Raven.Client.Documents.Operations.Attachments; -using System; -using System.Collections; -using System.Collections.Generic; using System.Linq.Expressions; -using zero.Core.Options; -namespace zero; +namespace zero.Persistence; public abstract class ZeroJavascriptIndex : AbstractJavaScriptIndexCreationTask, IZeroIndexDefinition { diff --git a/zero.Core/Persistence/ZeroIndexExtensions.cs b/zero.Core/Persistence/ZeroIndexExtensions.cs index ac6721ac..0f62504b 100644 --- a/zero.Core/Persistence/ZeroIndexExtensions.cs +++ b/zero.Core/Persistence/ZeroIndexExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace zero; +namespace zero.Persistence; public static class ZeroIndexExtensions { diff --git a/zero.Core/Persistence/ZeroStore.cs b/zero.Core/Persistence/ZeroStore.cs index 48df510f..b76a3287 100644 --- a/zero.Core/Persistence/ZeroStore.cs +++ b/zero.Core/Persistence/ZeroStore.cs @@ -1,8 +1,6 @@ using Raven.Client.Documents.Session; -using System.Collections.Generic; -using zero.Core.Options; -namespace zero; +namespace zero.Persistence; public class ZeroStore : IZeroStore { diff --git a/zero.Core/Rendering/Module.cs b/zero.Core/Rendering/Module.cs new file mode 100644 index 00000000..e485addd --- /dev/null +++ b/zero.Core/Rendering/Module.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Rendering; + +internal class RenderingModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Rendering/RazorRenderer.cs b/zero.Core/Rendering/RazorRenderer.cs new file mode 100644 index 00000000..41618baa --- /dev/null +++ b/zero.Core/Rendering/RazorRenderer.cs @@ -0,0 +1,283 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.IO; +using System.Text.Encodings.Web; + +namespace zero.Rendering; + +public class RazorRenderer : IRazorRenderer, IDisposable +{ + protected IRazorViewEngine ViewEngine { get; set; } + + protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; } + + protected IModelMetadataProvider ModelMetadataProvider { get; set; } + + protected IHttpContextAccessor HttpContextAccessor { get; set; } + + protected IServiceScope ServiceScope { get; set; } + + protected HtmlHelperOptions HtmlHelperOptions { get; set; } + + + public RazorRenderer(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory, + IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions mvcHelperOptions) + { + ViewEngine = viewEngine; + HttpContextAccessor = httpContextAccessor; + TempDataDictionaryFactory = tempDataDictionaryFactory; + ModelMetadataProvider = modelMetadataProvider; + ServiceScope = serviceProvider.CreateScope(); + HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions; + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(object args = null) where T : ViewComponent + { + return await ComponentAsync(typeof(T), args); + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(Type componentType, object args = null) + { + return await ComponentAsync(componentType, BuildActionContext(), args); + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(string componentName, object args = null) + { + return await ComponentAsync(componentName, BuildActionContext(), args); + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent + { + return await ComponentAsync(typeof(T), context, args); + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(Type componentType, ActionContext context, object args = null) + { + IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); + + using StringWriter stringWriter = new(); + + ViewContext viewContext = BuildViewContext(context, stringWriter); + + (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); + IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args); + + result.WriteTo(stringWriter, HtmlEncoder.Default); + await stringWriter.FlushAsync(); + return stringWriter.ToString(); + } + + + /// + /// Renders a razor component to a string + /// + public async Task ComponentAsync(string componentName, ActionContext context, object args = null) + { + IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); + + using StringWriter stringWriter = new(); + + ViewContext viewContext = BuildViewContext(context, stringWriter); + + (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); + IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args); + + result.WriteTo(stringWriter, HtmlEncoder.Default); + await stringWriter.FlushAsync(); + return stringWriter.ToString(); + } + + + /// + /// Renders a razor view to a string + /// + public async Task ViewAsync(string view, object model = null) + { + return await ViewAsync(BuildActionContext(), view, model); + } + + + /// + /// Renders a razor view to a string + /// + public async Task ViewAsync(ActionContext context, string view, object model = null) + { + IView viewResult = FindView(context, view); + + using StringWriter stringWriter = new(); + + ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult); + viewContext.RouteData = context.RouteData; + viewContext.ViewData.Model = model; + + await viewResult.RenderAsync(viewContext); + await stringWriter.FlushAsync(); + + return stringWriter.ToString(); + } + + + /// + public void Dispose() + { + ServiceScope?.Dispose(); + } + + + /// + /// Build the view context + /// + protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null) + { + ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...; + ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...; + + return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions); + } + + + /// + /// Builds a new view context + /// + protected virtual ActionContext BuildActionContext() + { + HttpContext context = GetHttpContext(); + RouteData routeData = context.GetRouteData(); + return new ActionContext(context, routeData, new ActionDescriptor()); + } + + + /// + /// Get HTTP context or mock one + /// + protected virtual HttpContext GetHttpContext() + { + HttpContext context = HttpContextAccessor.HttpContext; + context ??= new DefaultHttpContext() + { + RequestServices = ServiceScope.ServiceProvider + }; + + return context; + } + + + /// + /// Tries to find a view + /// + protected virtual IView FindView(ActionContext actionContext, string viewName) + { + ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false); + if (getViewResult.Success) + { + return getViewResult.View; + } + + ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false); + if (findViewResult.Success) + { + return findViewResult.View; + } + + IEnumerable searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations); + string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations)); + + throw new InvalidOperationException(errorMessage); + } + + + class GenericController : ControllerBase { } + + + class NullView : IView + { + public static readonly NullView Instance = new NullView(); + + public string Path => string.Empty; + + public Task RenderAsync(ViewContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.CompletedTask; + } + } + +} + + +public interface IRazorRenderer +{ + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(object args = null) where T : ViewComponent; + + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(Type componentType, object args = null); + + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(string componentName, object args = null); + + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent; + + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(Type componentType, ActionContext context, object args = null); + + /// + /// Renders a razor component to a string + /// + Task ComponentAsync(string componentName, ActionContext context, object args = null); + + /// + /// Renders a razor view to a string + /// + Task ViewAsync(string view, object model = null); + + /// + /// Renders a razor view to a string + /// + Task ViewAsync(ActionContext context, string view, object model = null); +} \ No newline at end of file diff --git a/zero.Routing/Configuration/RoutingEndpointOptions.cs b/zero.Core/Routing/Configuration/RoutingEndpointOptions.cs similarity index 100% rename from zero.Routing/Configuration/RoutingEndpointOptions.cs rename to zero.Core/Routing/Configuration/RoutingEndpointOptions.cs diff --git a/zero.Routing/Configuration/RoutingOptions.cs b/zero.Core/Routing/Configuration/RoutingOptions.cs similarity index 100% rename from zero.Routing/Configuration/RoutingOptions.cs rename to zero.Core/Routing/Configuration/RoutingOptions.cs diff --git a/zero.Routing/Configuration/RoutingPageResolverOptions.cs b/zero.Core/Routing/Configuration/RoutingPageResolverOptions.cs similarity index 88% rename from zero.Routing/Configuration/RoutingPageResolverOptions.cs rename to zero.Core/Routing/Configuration/RoutingPageResolverOptions.cs index f9e75e2c..da0db725 100644 --- a/zero.Routing/Configuration/RoutingPageResolverOptions.cs +++ b/zero.Core/Routing/Configuration/RoutingPageResolverOptions.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using zero.Core.Entities; +using System.Linq.Expressions; -namespace zero; +namespace zero.Routing; public class RoutingPageResolverOptions { diff --git a/zero.Routing/Entities/IRouteModel.cs b/zero.Core/Routing/Entities/IRouteModel.cs similarity index 100% rename from zero.Routing/Entities/IRouteModel.cs rename to zero.Core/Routing/Entities/IRouteModel.cs diff --git a/zero.Routing/Entities/Link.cs b/zero.Core/Routing/Entities/Link.cs similarity index 100% rename from zero.Routing/Entities/Link.cs rename to zero.Core/Routing/Entities/Link.cs diff --git a/zero.Routing/Entities/LinkPreview.cs b/zero.Core/Routing/Entities/LinkPreview.cs similarity index 100% rename from zero.Routing/Entities/LinkPreview.cs rename to zero.Core/Routing/Entities/LinkPreview.cs diff --git a/zero.Routing/Entities/LinkTarget.cs b/zero.Core/Routing/Entities/LinkTarget.cs similarity index 100% rename from zero.Routing/Entities/LinkTarget.cs rename to zero.Core/Routing/Entities/LinkTarget.cs diff --git a/zero.Routing/Entities/Route.cs b/zero.Core/Routing/Entities/Route.cs similarity index 100% rename from zero.Routing/Entities/Route.cs rename to zero.Core/Routing/Entities/Route.cs diff --git a/zero.Routing/Entities/RouteEndpoint.cs b/zero.Core/Routing/Entities/RouteEndpoint.cs similarity index 100% rename from zero.Routing/Entities/RouteEndpoint.cs rename to zero.Core/Routing/Entities/RouteEndpoint.cs diff --git a/zero.Routing/Entities/RouteRedirect.cs b/zero.Core/Routing/Entities/RouteRedirect.cs similarity index 93% rename from zero.Routing/Entities/RouteRedirect.cs rename to zero.Core/Routing/Entities/RouteRedirect.cs index 1e0ba8c2..ab8d7835 100644 --- a/zero.Routing/Entities/RouteRedirect.cs +++ b/zero.Core/Routing/Entities/RouteRedirect.cs @@ -1,6 +1,4 @@ -using zero.Core.Entities; - -namespace zero.Routing; +namespace zero.Routing; [RavenCollection("RouteRedirects")] public class RouteRedirect : ZeroEntity diff --git a/zero.Routing/Entities/RouteResponse.cs b/zero.Core/Routing/Entities/RouteResponse.cs similarity index 100% rename from zero.Routing/Entities/RouteResponse.cs rename to zero.Core/Routing/Entities/RouteResponse.cs diff --git a/zero.Routing/Entities/RoutingContext.cs b/zero.Core/Routing/Entities/RoutingContext.cs similarity index 92% rename from zero.Routing/Entities/RoutingContext.cs rename to zero.Core/Routing/Entities/RoutingContext.cs index 0177e22b..824825bf 100644 --- a/zero.Routing/Entities/RoutingContext.cs +++ b/zero.Core/Routing/Entities/RoutingContext.cs @@ -1,6 +1,4 @@ -using zero.Core; - -namespace zero.Routing; +namespace zero.Routing; public class RoutingContext : IRoutingContext { diff --git a/zero.Routing/Indexes/RouteRedirects_ByUrl.cs b/zero.Core/Routing/Indexes/RouteRedirects_ByUrl.cs similarity index 100% rename from zero.Routing/Indexes/RouteRedirects_ByUrl.cs rename to zero.Core/Routing/Indexes/RouteRedirects_ByUrl.cs diff --git a/zero.Routing/Indexes/Routes_ByDependencies.cs b/zero.Core/Routing/Indexes/Routes_ByDependencies.cs similarity index 100% rename from zero.Routing/Indexes/Routes_ByDependencies.cs rename to zero.Core/Routing/Indexes/Routes_ByDependencies.cs diff --git a/zero.Routing/Indexes/Routes_ForResolver.cs b/zero.Core/Routing/Indexes/Routes_ForResolver.cs similarity index 100% rename from zero.Routing/Indexes/Routes_ForResolver.cs rename to zero.Core/Routing/Indexes/Routes_ForResolver.cs diff --git a/zero.Routing/Links/ILinkProvider.cs b/zero.Core/Routing/Links/ILinkProvider.cs similarity index 100% rename from zero.Routing/Links/ILinkProvider.cs rename to zero.Core/Routing/Links/ILinkProvider.cs diff --git a/zero.Routing/Links/LinkTargetExtensions.cs b/zero.Core/Routing/Links/LinkTargetExtensions.cs similarity index 100% rename from zero.Routing/Links/LinkTargetExtensions.cs rename to zero.Core/Routing/Links/LinkTargetExtensions.cs diff --git a/zero.Routing/Links/Links.cs b/zero.Core/Routing/Links/Links.cs similarity index 100% rename from zero.Routing/Links/Links.cs rename to zero.Core/Routing/Links/Links.cs diff --git a/zero.Routing/Links/RawLinkProvider.cs b/zero.Core/Routing/Links/RawLinkProvider.cs similarity index 100% rename from zero.Routing/Links/RawLinkProvider.cs rename to zero.Core/Routing/Links/RawLinkProvider.cs diff --git a/zero.Routing/Module.cs b/zero.Core/Routing/Module.cs similarity index 100% rename from zero.Routing/Module.cs rename to zero.Core/Routing/Module.cs diff --git a/zero.Routing/NotFoundMatcherPolicy.cs b/zero.Core/Routing/NotFoundMatcherPolicy.cs similarity index 100% rename from zero.Routing/NotFoundMatcherPolicy.cs rename to zero.Core/Routing/NotFoundMatcherPolicy.cs diff --git a/zero.Routing/PageRouteProvider/BasePageRoute.cs b/zero.Core/Routing/PageRouteProvider/BasePageRoute.cs similarity index 100% rename from zero.Routing/PageRouteProvider/BasePageRoute.cs rename to zero.Core/Routing/PageRouteProvider/BasePageRoute.cs diff --git a/zero.Routing/PageRouteProvider/PageLinkProvider.cs b/zero.Core/Routing/PageRouteProvider/PageLinkProvider.cs similarity index 100% rename from zero.Routing/PageRouteProvider/PageLinkProvider.cs rename to zero.Core/Routing/PageRouteProvider/PageLinkProvider.cs diff --git a/zero.Routing/PageRouteProvider/PageRoute.cs b/zero.Core/Routing/PageRouteProvider/PageRoute.cs similarity index 78% rename from zero.Routing/PageRouteProvider/PageRoute.cs rename to zero.Core/Routing/PageRouteProvider/PageRoute.cs index 51cc7f99..5a91e856 100644 --- a/zero.Routing/PageRouteProvider/PageRoute.cs +++ b/zero.Core/Routing/PageRouteProvider/PageRoute.cs @@ -1,6 +1,4 @@ -using zero.Core.Entities; - -namespace zero.Routing; +namespace zero.Routing; public class PageRoute : BasePageRoute { diff --git a/zero.Routing/PageRouteProvider/PageRouteIdBuilder.cs b/zero.Core/Routing/PageRouteProvider/PageRouteIdBuilder.cs similarity index 86% rename from zero.Routing/PageRouteProvider/PageRouteIdBuilder.cs rename to zero.Core/Routing/PageRouteProvider/PageRouteIdBuilder.cs index c896d887..b9ba367f 100644 --- a/zero.Routing/PageRouteProvider/PageRouteIdBuilder.cs +++ b/zero.Core/Routing/PageRouteProvider/PageRouteIdBuilder.cs @@ -1,6 +1,4 @@ -using zero.Core.Entities; - -namespace zero.Routing; +namespace zero.Routing; public class PageRouteIdBuilder : IPageRouteIdBuilder { diff --git a/zero.Routing/PageRouteProvider/PageRouteProvider.cs b/zero.Core/Routing/PageRouteProvider/PageRouteProvider.cs similarity index 100% rename from zero.Routing/PageRouteProvider/PageRouteProvider.cs rename to zero.Core/Routing/PageRouteProvider/PageRouteProvider.cs diff --git a/zero.Core/Routing/PageRouteProvider/PageRouteResolverHelper.cs b/zero.Core/Routing/PageRouteProvider/PageRouteResolverHelper.cs new file mode 100644 index 00000000..8463c5d0 --- /dev/null +++ b/zero.Core/Routing/PageRouteProvider/PageRouteResolverHelper.cs @@ -0,0 +1,177 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using System.Collections.Concurrent; +using System.Linq.Expressions; + +namespace zero.Routing; + +public class PageRouteResolverHelper +{ + protected ConcurrentDictionary> PageRouteCache { get; set; } = new(); + + + public async Task ResolveFor(RoutingContext context, Func predicate = null) + { + return (await ResolveAllFor(context, predicate)).FirstOrDefault(); + } + + + public async Task ResolvePageFor(RoutingContext context, Func predicate = null) + { + return (await ResolveAllPagesFor(context, predicate)).FirstOrDefault(); + } + + + public async Task> ResolveAllFor(RoutingContext context, Func predicate = null) + { + string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; + + if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) + { + List pages = new(); + IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); + } + + Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); + Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); + + routes = new(); + + foreach ((string id, Page page) in idToPage) + { + if (idToRoute.TryGetValue(id, out Route route) && route != null) + { + routes.Add((page, route)); + } + } + + PageRouteCache.TryAdd(fullKey, routes); + } + + if (predicate != null) + { + return routes.Where(x => predicate(x.Item1)).Select(x => x.Item2).ToHashSet(); + } + + return routes.Select(x => x.Item2).ToHashSet(); + } + + + public async Task> ResolveAllPagesFor(RoutingContext context, Func predicate = null) + { + string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; + + if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) + { + List pages = new(); + IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); + } + + Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); + Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); + + routes = new(); + + foreach ((string id, Page page) in idToPage) + { + if (idToRoute.TryGetValue(id, out Route route) && route != null) + { + routes.Add((page, route)); + } + } + + PageRouteCache.TryAdd(fullKey, routes); + } + + if (predicate != null) + { + return routes.Where(x => predicate(x.Item1)).Select(x => x.Item1).ToHashSet(); + } + + return routes.Select(x => x.Item1).ToHashSet(); + } + + + public async Task> ResolveAllForAsDictionary(RoutingContext context, Func<(Page, Route), bool> predicate = null) + { + string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; + + if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) + { + List pages = new(); + IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); + } + + Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); + Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); + + routes = new(); + + foreach ((string id, Page page) in idToPage) + { + if (idToRoute.TryGetValue(id, out Route route) && route != null) + { + routes.Add((page, route)); + } + } + + PageRouteCache.TryAdd(fullKey, routes); + } + + if (predicate != null) + { + return routes.Where(x => predicate(x)).ToDictionary(x => x.Item1, x => x.Item2); + } + + return routes.ToDictionary(x => x.Item1, x => x.Item2); + } + + + public async Task IsRelevantFor(RoutingContext context, Page page) + { + HashSet pages = await ResolveAllPagesFor(context); + + if (pages.Any(x => x.Id == page.Id)) + { + return true; + } + + string[] childIds = await GetChildIds(context, page.Id); + return pages.Any(x => childIds.Contains(x.Id)); + } + + + /// + /// Get relevant pages for a page + /// + public virtual async Task> GetRelevantPagesFor(RoutingContext context, Page page) + { + HashSet pages = await ResolveAllPagesFor(context); + string[] childIds = await GetChildIds(context, page.Id); + return pages.Where(x => childIds.Contains(x.Id) || x.Id == page.Id).ToList(); + } + + + /// + /// Get parents for a page + /// + protected virtual async Task GetChildIds(RoutingContext context, params string[] pageIds) + { + return await context.Session.Query() + .Where(x => x.PathIds.In(pageIds)) + .Select(x => x.Id) + .ToArrayAsync(); + } +} diff --git a/zero.Routing/PageRouteProvider/PageUrlBuilder.cs b/zero.Core/Routing/PageRouteProvider/PageUrlBuilder.cs similarity index 98% rename from zero.Routing/PageRouteProvider/PageUrlBuilder.cs rename to zero.Core/Routing/PageRouteProvider/PageUrlBuilder.cs index a82b8aa5..3307d652 100644 --- a/zero.Routing/PageRouteProvider/PageUrlBuilder.cs +++ b/zero.Core/Routing/PageRouteProvider/PageUrlBuilder.cs @@ -1,5 +1,4 @@ using System.Text; -using zero.Core.Entities; namespace zero.Routing; diff --git a/zero.Routing/RedirectAutomation.cs b/zero.Core/Routing/RedirectAutomation.cs similarity index 100% rename from zero.Routing/RedirectAutomation.cs rename to zero.Core/Routing/RedirectAutomation.cs diff --git a/zero.Routing/RequestUrlResolver.cs b/zero.Core/Routing/RequestUrlResolver.cs similarity index 98% rename from zero.Routing/RequestUrlResolver.cs rename to zero.Core/Routing/RequestUrlResolver.cs index c699a99d..58cb6297 100644 --- a/zero.Routing/RequestUrlResolver.cs +++ b/zero.Core/Routing/RequestUrlResolver.cs @@ -1,8 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using zero.Core.Options; namespace zero.Routing; diff --git a/zero.Routing/RouteBulkRefresher.cs b/zero.Core/Routing/RouteBulkRefresher.cs similarity index 98% rename from zero.Routing/RouteBulkRefresher.cs rename to zero.Core/Routing/RouteBulkRefresher.cs index 953300f8..6684b957 100644 --- a/zero.Routing/RouteBulkRefresher.cs +++ b/zero.Core/Routing/RouteBulkRefresher.cs @@ -1,6 +1,5 @@ using Raven.Client.Documents; using Raven.Client.Documents.BulkInsert; -using zero.Core; namespace zero.Routing; diff --git a/zero.Routing/RouteExtensions.cs b/zero.Core/Routing/RouteExtensions.cs similarity index 100% rename from zero.Routing/RouteExtensions.cs rename to zero.Core/Routing/RouteExtensions.cs diff --git a/zero.Routing/RouteResolver.cs b/zero.Core/Routing/RouteResolver.cs similarity index 98% rename from zero.Routing/RouteResolver.cs rename to zero.Core/Routing/RouteResolver.cs index f5284c04..d6204b31 100644 --- a/zero.Routing/RouteResolver.cs +++ b/zero.Core/Routing/RouteResolver.cs @@ -3,8 +3,6 @@ using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Linq; using Raven.Client.Exceptions.Documents.Indexes; -using zero.Core; -using zero.Core.Extensions; namespace zero.Routing; diff --git a/zero.Routing/Routes.cs b/zero.Core/Routing/Routes.cs similarity index 99% rename from zero.Routing/Routes.cs rename to zero.Core/Routing/Routes.cs index ebf35125..a85329e1 100644 --- a/zero.Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Raven.Client.Documents.Linq; -using zero.Core; namespace zero.Routing; diff --git a/zero.Core/Routing/ZeroEndpointRouteBuilder.cs b/zero.Core/Routing/ZeroEndpointRouteBuilder.cs new file mode 100644 index 00000000..b403dcc1 --- /dev/null +++ b/zero.Core/Routing/ZeroEndpointRouteBuilder.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace zero.Routing; + +public class ZeroEndpointRouteBuilder : IZeroEndpointRouteBuilder +{ + IEndpointRouteBuilder _builder; + + public IServiceProvider ServiceProvider => _builder.ServiceProvider; + + public ICollection DataSources => _builder.DataSources; + + internal ZeroEndpointRouteBuilder(IEndpointRouteBuilder builder) + { + _builder = builder; + } + + public IApplicationBuilder CreateApplicationBuilder() => _builder.CreateApplicationBuilder(); +} + + +public interface IZeroEndpointRouteBuilder : IEndpointRouteBuilder +{ + +} \ No newline at end of file diff --git a/zero.Core/Routing/ZeroEnpointRouteBuilderExtensions.cs b/zero.Core/Routing/ZeroEnpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..8d24c4ff --- /dev/null +++ b/zero.Core/Routing/ZeroEnpointRouteBuilderExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Builder; + +namespace zero.Routing; + +public static class ZeroEndpointRouteBuilderExtensions +{ + public static IZeroEndpointRouteBuilder MapZeroBackoffice(this IZeroEndpointRouteBuilder endpoints, string path = "/zero") + { + //IZeroOptions options = app.ApplicationServices.GetService(); // TODO oO + // see https://our.umbraco.com/documentation/reference/routing/custom-routes#where-to-put-your-routing-logic + //string path = options.BackofficePath.EnsureStartsWith('/').TrimEnd('/'); + + endpoints.MapFallbackToController(path + "/{**path}", "Index", "ZeroBackoffice"); + return endpoints; + } + + + public static IZeroEndpointRouteBuilder MapZeroRoutes(this IZeroEndpointRouteBuilder endpoints) + { + endpoints.MapDynamicControllerRoute("/{**url}", state: null, order: 10); + return endpoints; + } +} diff --git a/zero.Routing/ZeroEntityRouteInterceptor.cs b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs similarity index 99% rename from zero.Routing/ZeroEntityRouteInterceptor.cs rename to zero.Core/Routing/ZeroEntityRouteInterceptor.cs index 5318e1b5..33d9b851 100644 --- a/zero.Routing/ZeroEntityRouteInterceptor.cs +++ b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Linq; -using zero.Core; namespace zero.Routing; diff --git a/zero.Routing/ZeroRouteProvider.cs b/zero.Core/Routing/ZeroRouteProvider.cs similarity index 100% rename from zero.Routing/ZeroRouteProvider.cs rename to zero.Core/Routing/ZeroRouteProvider.cs diff --git a/zero.Routing/ZeroRoutesTransformer.cs b/zero.Core/Routing/ZeroRoutesTransformer.cs similarity index 100% rename from zero.Routing/ZeroRoutesTransformer.cs rename to zero.Core/Routing/ZeroRoutesTransformer.cs diff --git a/zero.Core/Setup/SetupModel.cs b/zero.Core/Setup/SetupModel.cs new file mode 100644 index 00000000..967d50ac --- /dev/null +++ b/zero.Core/Setup/SetupModel.cs @@ -0,0 +1,29 @@ +namespace zero.Setup; + +public sealed class SetupModel +{ + public string AppName { get; set; } + + public SetupUserModel User { get; set; } + + public SetupDatabaseModel Database { get; set; } + + public string ContentRootPath { get; set; } +} + + +public sealed class SetupUserModel +{ + public string Name { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } +} + +public sealed class SetupDatabaseModel +{ + public string Url { get; set; } + + public string Name { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Setup/SetupModelValidator.cs b/zero.Core/Setup/SetupModelValidator.cs new file mode 100644 index 00000000..d0f708a1 --- /dev/null +++ b/zero.Core/Setup/SetupModelValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace zero.Setup; + +public class SetupModelValidator : ZeroValidator +{ + public SetupModelValidator() + { + RuleFor(x => x.User).NotNull(); + RuleFor(x => x.User.Email).NotEmpty().Email(); + RuleFor(x => x.User.Name).MaximumLength(40).NotEmpty(); + RuleFor(x => x.User.Password).MaximumLength(1024); // TODO password policy + + RuleFor(x => x.AppName).MaximumLength(40).NotEmpty(); + + RuleFor(x => x.Database.Url).NotEmpty().Url(); + RuleFor(x => x.Database.Name).NotEmpty(); + } +} \ No newline at end of file diff --git a/zero.Core/Usings.cs b/zero.Core/Usings.cs new file mode 100644 index 00000000..f2061baa --- /dev/null +++ b/zero.Core/Usings.cs @@ -0,0 +1,23 @@ + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; + +global using zero.Architecture; +global using zero.Configuration; +global using zero.Extensions; +global using zero.Identity; +global using zero.Pages; +global using zero.Validation; +global using zero.Utils; +global using zero.Persistence; +global using zero.Localization; +global using zero.Routing; +global using zero.Context; +global using zero.Communication; +global using zero.Rendering; +global using zero.Media; +global using zero.Applications; \ No newline at end of file diff --git a/zero.Core/Utils/ObjectCopycat.cs b/zero.Core/Utils/ObjectCopycat.cs index 2f63d6e2..a29274d1 100644 --- a/zero.Core/Utils/ObjectCopycat.cs +++ b/zero.Core/Utils/ObjectCopycat.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; -namespace zero; +namespace zero.Utils; public class ObjectCopycat { diff --git a/zero.Core/Utils/ObjectTraverser.cs b/zero.Core/Utils/ObjectTraverser.cs index 6f5505ce..8817cc16 100644 --- a/zero.Core/Utils/ObjectTraverser.cs +++ b/zero.Core/Utils/ObjectTraverser.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; -namespace zero; +namespace zero.Utils; public class ObjectTraverser { diff --git a/zero.Core/Utils/PrimitiveTypeCollection.cs b/zero.Core/Utils/PrimitiveTypeCollection.cs index 6c9921ac..05ae8f4b 100644 --- a/zero.Core/Utils/PrimitiveTypeCollection.cs +++ b/zero.Core/Utils/PrimitiveTypeCollection.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; -namespace zero; +namespace zero.Utils; public class PrimitiveTypeCollection : ConcurrentDictionary, IPrimitiveTypeCollection { diff --git a/zero.Core/Utils/TableBuilder/CsvCreator.cs b/zero.Core/Utils/TableBuilder/CsvCreator.cs index c2cd56b9..19369047 100644 --- a/zero.Core/Utils/TableBuilder/CsvCreator.cs +++ b/zero.Core/Utils/TableBuilder/CsvCreator.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.IO; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; -namespace zero; +namespace zero.Utils; public class CsvCreator : ITableCreator { diff --git a/zero.Core/Utils/TableBuilder/ExcelCreator.cs b/zero.Core/Utils/TableBuilder/ExcelCreator.cs index 70d3694e..5399a370 100644 --- a/zero.Core/Utils/TableBuilder/ExcelCreator.cs +++ b/zero.Core/Utils/TableBuilder/ExcelCreator.cs @@ -1,13 +1,8 @@ using ClosedXML.Excel; -using System; -using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -namespace zero; +namespace zero.Utils; public class ExcelCreator : ITableCreator { diff --git a/zero.Core/Utils/TableBuilder/ITableCreator.cs b/zero.Core/Utils/TableBuilder/ITableCreator.cs index 5bdd6133..c051ca80 100644 --- a/zero.Core/Utils/TableBuilder/ITableCreator.cs +++ b/zero.Core/Utils/TableBuilder/ITableCreator.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.IO; -using System.Threading; -using System.Threading.Tasks; -namespace zero; +namespace zero.Utils; public interface ITableCreator : IDisposable { diff --git a/zero.Core/Utils/TableBuilder/TableBuilder.cs b/zero.Core/Utils/TableBuilder/TableBuilder.cs index 22b78d5a..8369b7fe 100644 --- a/zero.Core/Utils/TableBuilder/TableBuilder.cs +++ b/zero.Core/Utils/TableBuilder/TableBuilder.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.IO; using System.Linq.Expressions; -using System.Threading.Tasks; -namespace zero; +namespace zero.Utils; public class TableBuilder : ITableBuilder, IDisposable { diff --git a/zero.Core/Utils/TableBuilder/TableColumn.cs b/zero.Core/Utils/TableBuilder/TableColumn.cs index 14b438c3..8a1d4deb 100644 --- a/zero.Core/Utils/TableBuilder/TableColumn.cs +++ b/zero.Core/Utils/TableBuilder/TableColumn.cs @@ -1,7 +1,6 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; -namespace zero; +namespace zero.Utils; public class TableColumn { diff --git a/zero.Core/Utils/TableBuilder/TableExport.cs b/zero.Core/Utils/TableBuilder/TableExport.cs index 62fc6f6a..08540f17 100644 --- a/zero.Core/Utils/TableBuilder/TableExport.cs +++ b/zero.Core/Utils/TableBuilder/TableExport.cs @@ -1,47 +1,43 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using zero.Core.Entities; +using System.IO; -namespace zero.Core.Utils +namespace zero.Utils; + +public abstract class TableExport : ITableExport where TEntity : ZeroIdEntity { - public abstract class TableExport : ITableExport where TEntity : ZeroIdEntity + public virtual async Task Export(TableFormat format = TableFormat.Excel) { - public virtual async Task Export(TableFormat format = TableFormat.Excel) - { - ITableBuilder builder = new TableBuilder(format); + ITableBuilder builder = new TableBuilder(format); - await Warmup(); + await Warmup(); - Build(builder); + Build(builder); - IAsyncEnumerable items = Load(); + IAsyncEnumerable items = Load(); - MemoryStream stream = await builder.ToStream(items); + MemoryStream stream = await builder.ToStream(items); - return stream; - } - - - protected virtual Task Warmup() => Task.CompletedTask; - - - protected virtual async IAsyncEnumerable Load() - { - await Task.Delay(0); - yield break; - } - - - protected virtual void Build(ITableBuilder builder) - { - builder.Column("Id").For(c => c.Id).Size(40); - } + return stream; } - public interface ITableExport where TEntity : ZeroIdEntity + protected virtual Task Warmup() => Task.CompletedTask; + + + protected virtual async IAsyncEnumerable Load() { - Task Export(TableFormat format = TableFormat.Excel); + await Task.Delay(0); + yield break; + } + + + protected virtual void Build(ITableBuilder builder) + { + builder.Column("Id").For(c => c.Id).Size(40); } } + + +public interface ITableExport where TEntity : ZeroIdEntity +{ + Task Export(TableFormat format = TableFormat.Excel); +} \ No newline at end of file diff --git a/zero.Core/Utils/TableBuilder/TableFormat.cs b/zero.Core/Utils/TableBuilder/TableFormat.cs index f604e020..f0eae05a 100644 --- a/zero.Core/Utils/TableBuilder/TableFormat.cs +++ b/zero.Core/Utils/TableBuilder/TableFormat.cs @@ -1,4 +1,4 @@ -namespace zero; +namespace zero.Utils; public enum TableFormat { diff --git a/zero.Core/Utils/TableBuilder/TableResult.cs b/zero.Core/Utils/TableBuilder/TableResult.cs index d2240d7a..62fbcef7 100644 --- a/zero.Core/Utils/TableBuilder/TableResult.cs +++ b/zero.Core/Utils/TableBuilder/TableResult.cs @@ -1,11 +1,10 @@ using System.IO; -namespace zero.Core.Utils -{ - public class TableResult - { - public MemoryStream Stream { get; set; } +namespace zero.Utils; - public string Filename { get; set; } - } -} +public class TableResult +{ + public MemoryStream Stream { get; set; } + + public string Filename { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Utils/TokenReplacement.cs b/zero.Core/Utils/TokenReplacement.cs new file mode 100644 index 00000000..1b3a7e3f --- /dev/null +++ b/zero.Core/Utils/TokenReplacement.cs @@ -0,0 +1,53 @@ +using System.Text.RegularExpressions; + +namespace zero.Utils; + +public class TokenReplacement +{ + static Regex TokenRegex; + + + static TokenReplacement() + { + TokenRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase); + } + + + public static string Apply(string text, Dictionary tokens) + { + if (text.IsNullOrWhiteSpace()) + { + return text; + } + + MatchCollection matches = TokenRegex.Matches(text); + + foreach (Match match in matches) + { + if (!match.Success) + { + continue; + } + + string original = match.Value; + string token = match.Groups[1].Value; + + if (tokens.ContainsKey(token)) + { + text = text.Replace(original, tokens[token]); + } + } + + return text; + + //foreach ((string key, string value) in tokens) + //{ + // string tokenKey = key.EnsureStartsWith(BEGIN).EnsureEndsWith(END); + // string tokenValue = value; // TODO escape for HTML? + + // text = text.Replace(tokenKey, value); + //} + + //return text; + } +} diff --git a/zero.Core/Validation/ValidatorCamelCasePropertyResolver.cs b/zero.Core/Validation/ValidatorCamelCasePropertyResolver.cs index 1cc7b2ff..52c87e72 100644 --- a/zero.Core/Validation/ValidatorCamelCasePropertyResolver.cs +++ b/zero.Core/Validation/ValidatorCamelCasePropertyResolver.cs @@ -1,9 +1,8 @@ using FluentValidation.Internal; -using System; using System.Linq.Expressions; using System.Reflection; -namespace zero; +namespace zero.Validation; public class ValidatorCamelCasePropertyResolver { diff --git a/zero.Core/Validation/ValidatorExtensions.cs b/zero.Core/Validation/ValidatorExtensions.cs new file mode 100644 index 00000000..b548cf8a --- /dev/null +++ b/zero.Core/Validation/ValidatorExtensions.cs @@ -0,0 +1,239 @@ +using FluentValidation; +using Raven.Client.Documents; +using System; +using System.Globalization; +using System.Linq; + +namespace zero.Validation; + +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.forms.hex_format"); + } + + + /// + /// Validate an email + /// + public static IRuleBuilderOptions Url(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must((root, value, context) => + { + return value.IsNullOrWhiteSpace() || Uri.IsWellFormedUriString(value, UriKind.Absolute); + }).WithMessage("@errors.forms.url_format"); + } + + + /// + /// 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.forms.email_invalid"); + } + + + /// + /// 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.forms.emails_invalid"); + } + + + /// + /// Check if this value is unique within a collection + /// + public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => + { + bool any = await store.Session().Advanced.AsyncDocumentQuery() + .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) + .WhereEquals(context.PropertyName.ToPascalCaseId(), value) + .AnyAsync(cancellation); + + return !any; + }).WithMessage("@errors.forms.not_unique"); + } + + + /// + /// Check if this value is at least set once to the expected value within a collection + /// + public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => + { + return await store.Session().Advanced.AsyncDocumentQuery() + .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) + .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) + .AnyAsync(cancellation); + }).WithMessage("@errors.forms.not_unique_alone"); + } + + + /// + /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) + /// + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity + { + return ruleBuilder.Exists(store); + } + + + /// + /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) + /// + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity where TReference : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => + { + if (id.IsNullOrWhiteSpace()) + { + return true; + } + + TReference model = await store.Session().LoadAsync(id); + return model != null; + }).WithMessage("@errors.forms.reference_notfound"); + } + + + /// + /// Validates a culture identifier + /// + public static IRuleBuilderOptions Culture(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must((root, value, context) => + { + if (value.IsNullOrWhiteSpace()) + { + return true; + } + + try + { + CultureInfo info = CultureInfo.GetCultureInfo(value); + return info != null && !info.EnglishName.Equals(value, StringComparison.InvariantCultureIgnoreCase); + } + catch + { + return false; + } + }).WithMessage("@errors.forms.culture"); + } + + + + + /// + /// Check if this value is unique within a collection + /// + public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => + { + bool any = await session.Advanced.AsyncDocumentQuery() + .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) + .WhereEquals(context.PropertyName.ToPascalCaseId(), value) + .AnyAsync(cancellation); + + return !any; + }).WithMessage("@errors.forms.not_unique"); + } + + + /// + /// Check if this value is at least set once to the expected value within a collection + /// + public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session, TProperty expectedValue) where T : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => + { + return await session.Advanced.AsyncDocumentQuery() + .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) + .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) + .AnyAsync(cancellation); + }).WithMessage("@errors.forms.not_unique_alone"); + } + + + /// + /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) + /// + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity + { + return ruleBuilder.Exists(session); + } + + + /// + /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) + /// + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity where TReference : ZeroEntity + { + return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => + { + if (id.IsNullOrWhiteSpace()) + { + return true; + } + + TReference model = await session.LoadAsync(id); + return model != null; + }).WithMessage("@errors.forms.reference_notfound"); + } +} diff --git a/zero.Core/Validation/ZeroValidator.cs b/zero.Core/Validation/ZeroValidator.cs index 6e1ad15d..2aba007a 100644 --- a/zero.Core/Validation/ZeroValidator.cs +++ b/zero.Core/Validation/ZeroValidator.cs @@ -4,7 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace zero; +namespace zero.Validation; public class ZeroValidator : ZeroValidator { diff --git a/zero.Core/zero.Core.csproj b/zero.Core/zero.Core.csproj index ad9a2756..a520d623 100644 --- a/zero.Core/zero.Core.csproj +++ b/zero.Core/zero.Core.csproj @@ -1,4 +1,4 @@ - + zero.Core diff --git a/zero.Routing/PageRouteProvider/PageRouteResolverHelper.cs b/zero.Routing/PageRouteProvider/PageRouteResolverHelper.cs deleted file mode 100644 index e36eed2e..00000000 --- a/zero.Routing/PageRouteProvider/PageRouteResolverHelper.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using zero.Core.Database.Indexes; -using zero.Core.Entities; - -namespace zero.Core.Routing -{ - public class PageRouteResolverHelper - { - protected ConcurrentDictionary> PageRouteCache { get; set; } = new(); - - - public async Task ResolveFor(RoutingContext context, Func predicate = null) - { - return (await ResolveAllFor(context, predicate)).FirstOrDefault(); - } - - - public async Task ResolvePageFor(RoutingContext context, Func predicate = null) - { - return (await ResolveAllPagesFor(context, predicate)).FirstOrDefault(); - } - - - public async Task> ResolveAllFor(RoutingContext context, Func predicate = null) - { - string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; - - if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) - { - List pages = new(); - IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); - - foreach (Expression> resolver in resolvers.Reverse()) - { - pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); - } - - Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); - Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); - - routes = new(); - - foreach ((string id, Page page) in idToPage) - { - if (idToRoute.TryGetValue(id, out Route route) && route != null) - { - routes.Add((page, route)); - } - } - - PageRouteCache.TryAdd(fullKey, routes); - } - - if (predicate != null) - { - return routes.Where(x => predicate(x.Item1)).Select(x => x.Item2).ToHashSet(); - } - - return routes.Select(x => x.Item2).ToHashSet(); - } - - - public async Task> ResolveAllPagesFor(RoutingContext context, Func predicate = null) - { - string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; - - if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) - { - List pages = new(); - IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); - - foreach (Expression> resolver in resolvers.Reverse()) - { - pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); - } - - Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); - Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); - - routes = new(); - - foreach ((string id, Page page) in idToPage) - { - if (idToRoute.TryGetValue(id, out Route route) && route != null) - { - routes.Add((page, route)); - } - } - - PageRouteCache.TryAdd(fullKey, routes); - } - - if (predicate != null) - { - return routes.Where(x => predicate(x.Item1)).Select(x => x.Item1).ToHashSet(); - } - - return routes.Select(x => x.Item1).ToHashSet(); - } - - - public async Task> ResolveAllForAsDictionary(RoutingContext context, Func<(Page, Route), bool> predicate = null) - { - string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; - - if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) - { - List pages = new(); - IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); - - foreach (Expression> resolver in resolvers.Reverse()) - { - pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); - } - - Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); - Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); - - routes = new(); - - foreach ((string id, Page page) in idToPage) - { - if (idToRoute.TryGetValue(id, out Route route) && route != null) - { - routes.Add((page, route)); - } - } - - PageRouteCache.TryAdd(fullKey, routes); - } - - if (predicate != null) - { - return routes.Where(x => predicate(x)).ToDictionary(x => x.Item1, x => x.Item2); - } - - return routes.ToDictionary(x => x.Item1, x => x.Item2); - } - - - public async Task IsRelevantFor(RoutingContext context, Page page) - { - HashSet pages = await ResolveAllPagesFor(context); - - if (pages.Any(x => x.Id == page.Id)) - { - return true; - } - - string[] childIds = await GetChildIds(context, page.Id); - return pages.Any(x => childIds.Contains(x.Id)); - } - - - /// - /// Get relevant pages for a page - /// - public virtual async Task> GetRelevantPagesFor(RoutingContext context, Page page) - { - HashSet pages = await ResolveAllPagesFor(context); - string[] childIds = await GetChildIds(context, page.Id); - return pages.Where(x => childIds.Contains(x.Id) || x.Id == page.Id).ToList(); - } - - - /// - /// Get parents for a page - /// - protected virtual async Task GetChildIds(RoutingContext context, params string[] pageIds) - { - return await context.Session.Query() - .Where(x => x.PathIds.In(pageIds)) - .Select(x => x.Id) - .ToArrayAsync(); - } - } -} diff --git a/zero.Routing/zero.Routing.csproj b/zero.Routing/zero.Routing.csproj deleted file mode 100644 index bde31072..00000000 --- a/zero.Routing/zero.Routing.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net6.0 - enable - - - - - - - diff --git a/zero.sln b/zero.sln index a82a969a..530d6d5f 100644 --- a/zero.sln +++ b/zero.sln @@ -45,11 +45,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{6FAA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Backoffice", "zero.Backoffice\zero.Backoffice.csproj", "{355432DA-01AF-4E1E-B87F-8A8D99E52411}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Routing", "zero.Routing\zero.Routing.csproj", "{C34E958D-24A8-442B-BAC8-096EE37D5ABF}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "old", "old", "{DECD7B35-5ECB-4B4B-BF62-B66F96B156FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -85,10 +83,6 @@ Global {355432DA-01AF-4E1E-B87F-8A8D99E52411}.Debug|Any CPU.Build.0 = Debug|Any CPU {355432DA-01AF-4E1E-B87F-8A8D99E52411}.Release|Any CPU.ActiveCfg = Release|Any CPU {355432DA-01AF-4E1E-B87F-8A8D99E52411}.Release|Any CPU.Build.0 = Release|Any CPU - {C34E958D-24A8-442B-BAC8-096EE37D5ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C34E958D-24A8-442B-BAC8-096EE37D5ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C34E958D-24A8-442B-BAC8-096EE37D5ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C34E958D-24A8-442B-BAC8-096EE37D5ABF}.Release|Any CPU.Build.0 = Release|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Release|Any CPU.ActiveCfg = Release|Any CPU