remove legacy project

This commit is contained in:
2024-07-24 10:45:14 +02:00
parent 0a9e062c15
commit 4edafac328
339 changed files with 0 additions and 20990 deletions
@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Builder;
namespace zero;
public static class ApplicationBuilderExtensions
{
public static IZeroApplicationBuilder UseZero(this IApplicationBuilder app) => new ZeroApplicationBuilder(app);
}
-43
View File
@@ -1,43 +0,0 @@
namespace zero.Applications;
/// <summary>
/// An application is a website or app. zero can host multiple websites at once which share common assets
/// </summary>
public class Application
{
/// <summary>
/// Raven database name for application data
/// </summary>
public string Database { get; set; }
/// <summary>
/// Full company or product name
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Generic contact email. Can be used in various locations
/// </summary>
public string Email { get; set; }
/// <summary>
/// Image of the application
/// </summary>
public string ImageId { get; set; }
/// <summary>
/// Simple image of the application (can be used as favicon)
/// </summary>
public string IconId { get; set; }
/// <summary>
/// All assigned domains for this application
/// </summary>
public Uri[] Domains { get; set; } = Array.Empty<Uri>();
/// <summary>
/// Features which are enabled for this application.
/// Can be user-defined and affect both backoffice and frontend
/// </summary>
public List<string> Features { get; set; } = new();
}
@@ -1,12 +0,0 @@
//namespace zero.Applications;
//public class ApplicationOptions
//{
// public ApplicationOptions()
// {
// EnableMultiple = false;
// }
// public bool EnableMultiple { get; set; }
//}
@@ -1,35 +0,0 @@
namespace zero.Applications;
public class ApplicationRegistration
{
/// <summary>
/// Alias for this tenant
/// </summary>
public string Key { get; set; }
/// <summary>
/// Name of the tenant
/// </summary>
public string Name { get; set; }
/// <summary>
/// Raven database name for application data
/// </summary>
public string Database { get; set; }
/// <summary>
/// Generic contact email. Can be used in various locations
/// </summary>
public string Email { get; set; }
/// <summary>
/// All assigned domains for this application
/// </summary>
public Uri[] Domains { get; set; } = Array.Empty<Uri>();
/// <summary>
/// Features which are enabled for this application.
/// Can be user-defined and affect both backoffice and frontend
/// </summary>
public List<string> Features { get; set; } = new();
}
@@ -1,28 +0,0 @@
namespace zero.Applications;
public class ApplicationRegistry : IApplicationRegistry
{
protected IZeroOptions Options { get; set; }
public ApplicationRegistry(IZeroOptions options)
{
Options = options;
}
/// <inheritdoc />
public Task<IEnumerable<ApplicationRegistration>> GetAll()
{
return Task.FromResult<IEnumerable<ApplicationRegistration>>(Options.Applications);
}
}
public interface IApplicationRegistry
{
/// <summary>
/// Get all registered applications
/// </summary>
Task<IEnumerable<ApplicationRegistration>> GetAll();
}
@@ -1,260 +0,0 @@
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<ApplicationResolver> Logger { get; private set; }
protected IHandlerHolder Handler { get; private set; }
IList<Application> _apps;
/// <summary>
/// The system application is bound to the context in case no application was resolved
/// </summary>
SystemApplication _systemApplication;
public ApplicationResolver(IZeroStore store, IZeroOptions options, ILogger<ApplicationResolver> logger, IHandlerHolder handler = null)
{
Store = store;
Options = options;
Logger = logger;
Handler = handler;
_systemApplication = new()
{
Id = "system",
Name = "zero system",
Database = Options.For<RavenOptions>().Database,
Alias = "system"
};
}
/// <inheritdoc />
public async Task<Application> Resolve(HttpContext context, ClaimsPrincipal user)
{
if (context?.Request == null)
{
Logger.LogWarning("Could not resolve application as HTTP request is null");
return _systemApplication;
}
Application app;
if (context.IsBackofficeRequest(Options.ZeroPath))
{
ZeroUser userEntity = await GetBackofficeUser(user);
app = await ResolveFromBackofficeHandlers(context, userEntity) ?? _systemApplication;
}
else
{
app = await ResolveFromHandlers(context) ?? await ResolveFromRequest(context);
}
if (app == null)
{
Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host);
return _systemApplication;
}
return app;
}
/// <inheritdoc />
public async Task<Application> ResolveFromHandlers(HttpContext context)
{
IList<Application> apps = await GetApplications();
IEnumerable<IApplicationResolverHandler> handlers = Handler.GetAll<IApplicationResolverHandler>();
foreach (IApplicationResolverHandler handler in handlers)
{
if (handler.TryResolve(context, apps, out Application resolved))
{
return resolved;
}
}
return null;
}
/// <inheritdoc />
public async Task<Application> ResolveFromBackofficeHandlers(HttpContext context, ZeroUser user)
{
IList<Application> apps = await GetApplications();
IEnumerable<IBackofficeApplicationResolverHandler> handlers = Handler.GetAll<IBackofficeApplicationResolverHandler>();
foreach (IBackofficeApplicationResolverHandler handler in handlers)
{
if (handler.TryResolve(context, apps, user, out Application resolved))
{
return resolved;
}
}
return null;
}
/// <inheritdoc />
public async Task<Application> ResolveFromUser(ClaimsPrincipal user)
{
ZeroUser userEntity = await GetBackofficeUser(user);
return await ResolveFromUser(userEntity);
}
/// <inheritdoc />
public async Task<Application> ResolveFromUser(ZeroUser 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<Application>(appId);
}
/// <inheritdoc />
public Task<Application> ResolveFromRequest(HttpContext context) => ResolveFromUri(context.Request.GetEncodedUrl());
/// <inheritdoc />
public async Task<Application> ResolveFromUri(string uriString) => ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications());
/// <inheritdoc />
public async Task<Application> ResolveFromUri(Uri uri) => ResolveFromUriInternal(uri, await GetApplications());
/// <summary>
/// Get matching application from an URI
/// </summary>
Application ResolveFromUriInternal(Uri uri, IList<Application> 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;
}
/// <summary>
/// Get all applications to choose from
/// </summary>
async Task<IList<Application>> GetApplications()
{
if (_apps != null)
{
return _apps;
}
IAsyncDocumentSession session = Store.Session(global: true);
_apps = await session.Query<Application>().ToListAsync();
return _apps;
}
/// <summary>
/// Get backoffice user from claims principal
/// </summary>
async Task<ZeroUser> GetBackofficeUser(ClaimsPrincipal user)
{
string userId = user.FindFirstValue(Constants.Auth.Claims.UserId);
IAsyncDocumentSession session = Store.Session(global: true);
return await session.LoadAsync<ZeroUser>(userId);
}
}
public interface IApplicationResolver
{
/// <summary>
/// Resolves the current application from either the backoffice user (in case it is backoffice request)
/// or the domain (in case it is frontend request).
/// </summary>
Task<Application> Resolve(HttpContext context, ClaimsPrincipal user);
/// <summary>
/// Resolves the current application from the request path
/// </summary>
Task<Application> ResolveFromRequest(HttpContext context);
/// <summary>
/// Get matching application from an URI string
/// </summary>
Task<Application> ResolveFromUri(string uriString);
/// <summary>
/// Get matching application from an URI
/// </summary>
Task<Application> ResolveFromUri(Uri uri);
/// <summary>
/// Resolves the current application from the logged-in backoffice user.
/// This method won't return apps the user has no access to.
/// </summary>
Task<Application> ResolveFromUser(ClaimsPrincipal user);
/// <summary>
/// Resolves the current application from a user.
/// This method won't return apps the user has no access to.
/// </summary>
Task<Application> ResolveFromUser(ZeroUser user);
}
@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace zero.Applications;
public interface IApplicationResolverHandler : IHandler
{
bool TryResolve(HttpContext context, IEnumerable<Application> applications, out Application resolved);
}
@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace zero.Applications;
public interface IBackofficeApplicationResolverHandler : IHandler
{
bool TryResolve(HttpContext context, IEnumerable<Application> applications, ZeroUser user, out Application resolved);
}
@@ -1,5 +0,0 @@
namespace zero.Applications;
public sealed class SystemApplication : Application
{
}
@@ -1,14 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Applications;
internal class ZeroApplicationModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IApplicationResolver, ApplicationResolver>();
services.AddScoped<IApplicationRegistry, ApplicationRegistry>();
//services.AddOptions<ApplicationOptions>().Bind(configuration.GetSection("Zero:Applications"));
}
}
@@ -1,161 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyModel;
using System.Reflection;
namespace zero.Architecture;
public class AssemblyDiscovery : IAssemblyDiscovery
{
public static IAssemblyDiscovery Current;
AssemblyDiscoveryContext Context;
IMvcBuilder Mvc;
public AssemblyDiscovery(IMvcBuilder mvc)
{
Mvc = mvc;
Context = new AssemblyDiscoveryContext();
Current = this;
}
/// <inheritdoc />
public void Execute(IEnumerable<IAssemblyDiscoveryRule> rules)
{
List<Assembly> assemblies = new List<Assembly>();
DependencyContext dependencyContext = DependencyContext.Load(Context.EntryAssembly);
if (dependencyContext == null)
{
return;
}
string[] existingLibs = Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Name).ToArray();
IEnumerable<RuntimeLibrary> libraries = dependencyContext.RuntimeLibraries.Where(lib => !existingLibs.Contains(lib.Name, StringComparer.OrdinalIgnoreCase));
foreach (RuntimeLibrary library in libraries)
{
if (rules.Any(rule => rule.IsValid(library, Context)))
{
IEnumerable<Assembly> libraryAssemblies = library.GetDefaultAssemblyNames(dependencyContext).Select(name => Assembly.Load(name));
foreach (Assembly assembly in libraryAssemblies)
{
Mvc.AddApplicationPart(assembly);
}
}
}
}
/// <inheritdoc />
public void AddAssembly(Assembly assembly)
{
string[] existingLibs = Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Name).ToArray();
if (!existingLibs.Contains(assembly.GetName().Name))
{
Mvc.AddApplicationPart(assembly);
}
}
/// <inheritdoc />
public IEnumerable<TypeInfo> GetTypes<TService>(bool allowGenerics = false) => GetTypes(typeof(TService), allowGenerics);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetTypes<TService>(IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetTypes(typeof(TService), parts, allowGenerics);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetTypes(Type serviceType, bool allowGenerics = false) => GetAllClassTypes(allowGenerics).Where(t => serviceType.GetTypeInfo().IsAssignableFrom(t) && t.AsType() != serviceType);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetTypes(Type serviceType, IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetAllClassTypes(parts, allowGenerics).Where(t => serviceType.GetTypeInfo().IsAssignableFrom(t) && t.AsType() != serviceType);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetAllClassTypes(bool allowGenerics = false) => GetAllTypes().Where(t => t.IsClass && !t.IsAbstract && (allowGenerics || !t.ContainsGenericParameters));
/// <inheritdoc />
public IEnumerable<TypeInfo> GetAllClassTypes(IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetAllTypes(parts).Where(t => t.IsClass && !t.IsAbstract && (allowGenerics || !t.ContainsGenericParameters));
/// <inheritdoc />
public IEnumerable<Assembly> GetAssemblies() => Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Assembly);
/// <inheritdoc />
public IEnumerable<Assembly> GetAssemblies(IEnumerable<ApplicationPart> parts) => parts.OfType<AssemblyPart>().Select(p => p.Assembly);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetAllTypes() => Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().SelectMany(p => p.Types);
/// <inheritdoc />
public IEnumerable<TypeInfo> GetAllTypes(IEnumerable<ApplicationPart> parts) => parts.OfType<AssemblyPart>().SelectMany(p => p.Types);
}
public interface IAssemblyDiscovery
{
/// <summary>
/// Discovers runtime assemblies based on the given rules
/// </summary>
void Execute(IEnumerable<IAssemblyDiscoveryRule> rules);
/// <summary>
/// Manually add an assembly
/// </summary>
void AddAssembly(Assembly assembly);
/// <summary>
/// Get all discovered types which implement a certain service
/// </summary>
IEnumerable<TypeInfo> GetTypes<TService>(bool allowGenerics = false);
/// <summary>
/// Get all discovered types which implement a certain service
/// </summary>
IEnumerable<TypeInfo> GetTypes<TService>(IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
/// <summary>
/// Get all discovered types which implement a certain service
/// </summary>
IEnumerable<TypeInfo> GetTypes(Type serviceType, bool allowGenerics = false);
/// <summary>
/// Get all discovered types which implement a certain service
/// </summary>
IEnumerable<TypeInfo> GetTypes(Type serviceType, IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
/// <summary>
/// Get all registered types
/// </summary>
IEnumerable<TypeInfo> GetAllTypes();
/// <summary>
/// Get all registered types
/// </summary>
IEnumerable<TypeInfo> GetAllTypes(IEnumerable<ApplicationPart> parts);
/// <summary>
/// Get all registered types
/// </summary>
IEnumerable<TypeInfo> GetAllClassTypes(bool allowGenerics = false);
/// <summary>
/// Get all registered types
/// </summary>
IEnumerable<TypeInfo> GetAllClassTypes(IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
/// <summary>
/// Get all registered assemblies
/// </summary>
IEnumerable<Assembly> GetAssemblies();
/// <summary>
/// Get all registered assemblies
/// </summary>
IEnumerable<Assembly> GetAssemblies(IEnumerable<ApplicationPart> parts);
}
@@ -1,26 +0,0 @@
using System.Reflection;
namespace zero.Architecture;
public class AssemblyDiscoveryContext
{
public Assembly EntryAssembly { get; private set; }
public string EntryAssemblyName { get; private set; }
public bool HasEntryAssembly => EntryAssembly != null;
public AssemblyDiscoveryContext()
{
EntryAssembly = Assembly.GetEntryAssembly();
EntryAssemblyName = EntryAssembly?.GetName().Name;
}
public AssemblyDiscoveryContext(Assembly entryAssembly)
{
EntryAssembly = entryAssembly;
EntryAssemblyName = entryAssembly.GetName().Name;
}
}
@@ -1,12 +0,0 @@
using Microsoft.Extensions.DependencyModel;
namespace zero.Architecture;
public interface IAssemblyDiscoveryRule
{
/// <summary>
/// Returns true if the specified runtime library should be added to
/// the ApplicationPartManager; otherwise false.
/// </summary>
bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context);
}
@@ -1,16 +0,0 @@
using Microsoft.Extensions.DependencyModel;
namespace zero.Architecture;
public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule
{
const string ZERO_PREFIX = "zero.";
/// <inheritdoc />
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));
}
}
@@ -1,216 +0,0 @@
using System.Linq.Expressions;
using System.Text.Json.Serialization;
namespace zero.Architecture;
public sealed class DefaultShallowBlueprint : Blueprint<ZeroEntity>
{
public DefaultShallowBlueprint(Type type) : base()
{
Alias = "shallow.default";
ContentType = type;
LockAll();
}
protected override BlueprintField<ZeroEntity> AddField(Expression<Func<ZeroEntity, object>> selector)
{
throw new InvalidOperationException("Shallow blueprints can not contain unlocked fields");
}
}
/// <summary>
///
/// </summary>
public class Blueprint<T> : Blueprint where T : ZeroEntity, new()
{
public List<BlueprintField<T>> UnlockedFields { get; private set; } = new();
/// <summary>
/// These fields are not copied by the ObjectCloner
/// as they are either locked or copied manually by ApplyDefaults()
/// </summary>
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)) { }
/// <summary>
/// Merges blueprint data into a model based on the configuration
/// </summary>
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);
ObjectCopycat.CopyProperties(blueprint, model, DefaultUncopyFields.Union(model.Blueprint.Desync).ToArray());
return model;
}
/// <inheritdoc />
public override ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model) => Apply(blueprint as T, model as T);
/// <summary>
/// Update default entity data
/// </summary>
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;
}
/// <summary>
/// Lock a field so it always get synchronized no and cannot be changed
/// </summary>
public virtual void LockAll()
{
UnlockedFields = new();
}
/// <summary>
/// Lock a field so it always get synchronized no and cannot be changed
/// </summary>
public virtual void Lock(Expression<Func<T, object>> selector)
{
RemoveField(selector);
}
public virtual void Unlock(Expression<Func<T, object>> selector)
{
AddField(selector);
}
public virtual void UnlockDefaults()
{
AddField(x => x.Name);
AddField(x => x.Alias);
AddField(x => x.Sort);
AddField(x => x.Key);
AddField(x => x.IsActive);
}
/// <inheritdoc />
public override IEnumerable<string> GetUnlockedFieldNames()
{
foreach (BlueprintField<T> field in UnlockedFields)
{
yield return field.FieldName.ToCamelCaseId();
}
}
/// <summary>
/// Get existing field or create a new one
/// </summary>
protected virtual BlueprintField<T> AddField(Expression<Func<T, object>> selector)
{
BlueprintField<T> tempField = new(selector);
BlueprintField<T> storedField = UnlockedFields.FirstOrDefault(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase));
if (storedField != null)
{
return storedField;
}
UnlockedFields.Add(tempField);
return tempField;
}
/// <summary>
/// Removes a field
/// </summary>
protected virtual void RemoveField(Expression<Func<T, object>> selector)
{
BlueprintField<T> tempField = new(selector);
if (tempField != null)
{
UnlockedFields.RemoveAll(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase));
}
}
}
/// <summary>
///
/// </summary>
public abstract class Blueprint
{
/// <summary>
/// Type of the associated entity
/// </summary>
[JsonIgnore]
public Type ContentType { get; protected set; }
public string Alias { get; protected set; }
/// <summary>
/// String comparer for property name comparison
/// </summary>
protected readonly StringComparer StringComparer = StringComparer.InvariantCultureIgnoreCase;
public Blueprint(Type type)
{
ContentType = type;
Alias = type.Name.ToCamelCaseId();
}
/// <summary>
/// Merges blueprint data into a model based on the configuration
/// </summary>
public abstract ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model);
/// <summary>
/// Get all fields which have been unlocked
/// </summary>
public abstract IEnumerable<string> GetUnlockedFieldNames();
}
public class DefaultBlueprint<T> : Blueprint<T> where T : ZeroEntity, new()
{
public DefaultBlueprint(Action<Blueprint<T>> expression = null) : base()
{
expression?.Invoke(this);
}
}
@@ -1,32 +0,0 @@
namespace zero.Architecture;
public class BlueprintChildInterceptor : Interceptor<ZeroEntity>, IBlueprintInterceptor
{
string configuredZeroDatabase;
public BlueprintChildInterceptor(IZeroContext context)
{
Gravity = -1;
configuredZeroDatabase = context.Options.For<RavenOptions>().Database;
}
/// <summary>
/// Only run this interceptor when the database is an app database
/// </summary>
public override bool CanHandle(InterceptorParameters args, Type modelType) => args.Context.Store.ResolvedDatabase != configuredZeroDatabase;
/// <inheritdoc />
public override Task<InterceptorResult<ZeroEntity>> Deleting(InterceptorParameters args, ZeroEntity model)
{
if (model.Blueprint == null)
{
return Task.FromResult<InterceptorResult<ZeroEntity>>(default);
}
InterceptorResult<ZeroEntity> result = new();
result.Result = Result<ZeroEntity>.Fail("@blueprint.errors.cannotDeleteChild");
return Task.FromResult(result);
}
}
@@ -1,27 +0,0 @@
namespace zero.Architecture;
/// <summary>
/// Defines a base entity which is synced and properties which are overridden
/// </summary>
public class BlueprintConfiguration
{
/// <summary>
/// Id of the entity the synchronisation is based upon
/// </summary>
public string TargetId { get; set; }
/// <summary>
/// A shallow copy of a blueprint can not be changed and is always fully synchronised with the parent entity
/// </summary>
public bool IsShallow { get; set; }
/// <summary>
/// Properties which are not synced and have their own values
/// </summary>
public string[] Desync { get; set; } = Array.Empty<string>();
/// <summary>
/// Additional custom sync options
/// </summary>
public Dictionary<string, string> Options = new();
}
@@ -1,54 +0,0 @@
using System.Linq.Expressions;
using System.Reflection;
namespace zero.Architecture;
public class BlueprintField<T> where T : ZeroEntity
{
public Expression<Func<T, object>> Expression { get; private set; }
public PropertyInfo PropertyInfo { get; private set; }
public string FieldName { get; private set; }
Action<T, T> _applyHook { get; set; }
Func<T, object> _selectorFunc = null;
internal BlueprintField(Expression<Func<T, object>> 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<T, T> onUpdate)
{
_applyHook = onUpdate;
}
}
@@ -1,137 +0,0 @@
using Microsoft.Extensions.Logging;
using Raven.Client.Documents;
using Raven.Client.Documents.Session;
namespace zero.Architecture;
public class BlueprintInterceptor : Interceptor<ZeroEntity>, IBlueprintInterceptor
{
protected IZeroContext Context { get; set; }
protected IZeroStore Store { get; set; }
protected ILogger<BlueprintInterceptor> Logger { get; set; }
protected IBlueprintService BlueprintService { get; set; }
IList<Application> apps;
string configuredZeroDatabase;
public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger<BlueprintInterceptor> logger, IBlueprintService blueprintService)
{
Gravity = -1;
Context = context;
Store = store;
Logger = logger;
BlueprintService = blueprintService;
configuredZeroDatabase = context.Options.For<RavenOptions>().Database;
}
/// <summary>
/// Only run when operations are on the zero database
/// </summary>
public override bool CanHandle(InterceptorParameters args, Type modelType) => args.Context.Store.ResolvedDatabase == configuredZeroDatabase;
/// <inheritdoc />
public override Task Created(InterceptorParameters args, ZeroEntity model) => Saved(args, model, false);
/// <inheritdoc />
public override Task Updated(InterceptorParameters args, ZeroEntity model) => Saved(args, model, true);
/// <inheritdoc />
public async Task Saved(InterceptorParameters args, ZeroEntity model, bool update = false)
{
if (!BlueprintService.TryGetBlueprint(model, out Blueprint blueprint))
{
blueprint = new DefaultShallowBlueprint(model.GetType());
}
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<ZeroEntity>(model.Id);
if (child == null)
{
child = ObjectCopycat.Clone(model);
child.Blueprint = new()
{
TargetId = model.Id
};
}
else
{
blueprint.Apply(model, child);
}
count += 1;
InterceptorInstruction<ZeroEntity> interceptor = update ? args.Interceptors.ForUpdate(model) : args.Interceptors.ForCreate(model);
interceptor.Filter(x => x is not IBlueprintInterceptor);
await interceptor.Start(args.Operations);
await session.StoreAsync(child);
await session.SaveChangesAsync();
await interceptor.Complete();
await session.SaveChangesAsync();
}
Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, model.Name, model.Id);
}
/// <inheritdoc />
public override async Task Deleted(InterceptorParameters args, ZeroEntity model)
{
if (!BlueprintService.TryGetBlueprint(model, out Blueprint blueprint))
{
return;
}
int count = 0;
foreach (Application app in await GetApplications())
{
using ZeroContextScope scope = Context.CreateScope(app);
IZeroDocumentSession session = scope.Store.Session(scope.Database);
count += 1;
InterceptorInstruction<ZeroEntity> interceptor = args.Interceptors.ForDelete(model);
interceptor.Filter(x => x is not IBlueprintInterceptor);
await interceptor.Start(args.Operations);
session.Delete(model.Id);
await session.SaveChangesAsync();
await interceptor.Complete();
await session.SaveChangesAsync();
}
Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, model.Name, model.Id);
}
/// <summary>
/// Get all applications to choose from
/// </summary>
async Task<IList<Application>> GetApplications()
{
if (apps != null)
{
return apps;
}
IAsyncDocumentSession session = Store.Session(global: true);
apps = await session.Query<Application>().ToListAsync();
return apps;
}
}
@@ -1,21 +0,0 @@
namespace zero.Architecture;
public class BlueprintOptions : List<Blueprint>
{
public bool Enabled { get; set; } = false;
public void Add<T>() where T : Blueprint, new()
{
base.Add(new T());
}
public void Add<T>(Blueprint<T> implementation) where T : ZeroEntity, new()
{
base.Add(implementation);
}
public void Add<T>(Action<Blueprint<T>> createExpression = null) where T : ZeroEntity, new()
{
base.Add(new DefaultBlueprint<T>(createExpression));
}
}
@@ -1,76 +0,0 @@
namespace zero.Architecture;
public class BlueprintService : IBlueprintService
{
protected IZeroOptions Options { get; set; }
protected IReadOnlyCollection<Blueprint> Blueprints { get; set; }
public BlueprintService(IZeroOptions options)
{
Options = options;
Blueprints = Options.For<BlueprintOptions>();
}
/// <inheritdoc />
public bool IsEnabled<T>(T model) => IsEnabled(model.GetType());
/// <inheritdoc />
public bool IsEnabled<T>()=> IsEnabled(typeof(T));
/// <inheritdoc />
public bool IsEnabled(Type type) => Blueprints.Any(x => x.ContentType.IsAssignableFrom(type));
/// <inheritdoc />
public bool TryGetBlueprint<T>(T model, out Blueprint blueprint) => TryGetBlueprint(model.GetType(), out blueprint);
/// <inheritdoc />
public bool TryGetBlueprint<T>(out Blueprint blueprint) => TryGetBlueprint(typeof(T), out blueprint);
/// <inheritdoc />
public bool TryGetBlueprint(Type type, out Blueprint blueprint)
{
blueprint = Blueprints.FirstOrDefault(x => x.ContentType.IsAssignableFrom(type));
return blueprint != null;
}
/// <inheritdoc />
public Dictionary<Type, Blueprint> GetAllBlueprints()
{
return Blueprints.ToDictionary(x => x.ContentType, x => x);
}
}
public interface IBlueprintService
{
/// <summary>
/// Check whether blueprinting functionality is enabled for a certain entity
/// </summary>
bool IsEnabled<T>();
/// <summary>
/// Check whether blueprinting functionality is enabled for a certain entity
/// </summary>
bool IsEnabled<T>(T model);
bool IsEnabled(Type type);
bool TryGetBlueprint<T>(T model, out Blueprint blueprint);
bool TryGetBlueprint<T>(out Blueprint blueprint);
bool TryGetBlueprint(Type type, out Blueprint blueprint);
/// <summary>
/// Get all registered blueprint configurations
/// </summary>
Dictionary<Type, Blueprint> GetAllBlueprints();
}
@@ -1,17 +0,0 @@
namespace zero.Architecture;
public enum BlueprintStatus
{
/// <summary>
/// This entity is standalone
/// </summary>
Standalone = 0,
/// <summary>
/// This entity is a blueprint
/// </summary>
Parent = 1,
/// <summary>
/// This entity is a child of blueprint
/// </summary>
Child = 2
}
@@ -1,6 +0,0 @@
namespace zero.Architecture;
public interface IBlueprintInterceptor
{
}
@@ -1,6 +0,0 @@
namespace zero.Architecture;
internal interface IZeroBuiltInPlugin
{
}
@@ -1,25 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
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(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { }
}
public interface IZeroPlugin
{
IZeroPluginOptions Options { get; }
void ConfigureServices(IServiceCollection services, IConfiguration configuration);
void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider);
}
@@ -1,30 +0,0 @@
namespace zero.Architecture;
public class ZeroPluginOptions : IZeroPluginOptions
{
public string Name { get; set; }
public string Description { get; set; }
public List<string> LocalizationPaths { get; private set; } = new List<string>();
public string PluginPath { get; set; }
}
public interface IZeroPluginOptions
{
string Name { get; set; }
string Description { get; set; }
List<string> LocalizationPaths { get; }
string PluginPath { get; set; }
}
public interface IZeroPluginStartup
{
Task Startup();
}
@@ -1,15 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Architecture;
internal class ZeroArchitectureModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IBlueprintService, BlueprintService>();
//services.AddScoped<IInterceptor, BlueprintInterceptor>();
//services.AddScoped<IInterceptor, BlueprintChildInterceptor>();
services.AddOptions<BlueprintOptions>().Bind(configuration.GetSection("Zero:Blueprints"));
}
}
-50
View File
@@ -1,50 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Architecture;
public abstract class ZeroModule : IZeroModule
{
/// <inheritdoc />
public virtual int Order { get; } = 0;
/// <inheritdoc />
public virtual int ConfigureOrder => Order;
/// <inheritdoc />
public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { }
/// <inheritdoc />
public virtual void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { }
}
public interface IZeroModule
{
/// <summary>
/// Get the value to use to order startups to configure services. The default is 0.
/// </summary>
int Order { get; }
/// <summary>
/// Get the value to use to order startups to build the pipeline. The default is the 'Order' property.
/// </summary>
int ConfigureOrder { get; }
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
/// </summary>
/// <param name="services">The collection of service descriptors.</param>
void ConfigureServices(IServiceCollection services, IConfiguration configuration);
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="builder"></param>
/// <param name="routes"></param>
/// <param name="serviceProvider"></param>
void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider);
}
@@ -1,70 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
namespace zero.Architecture;
public class ZeroModuleCollection : ZeroModule
{
ConcurrentDictionary<Type, IZeroModule> _modules = new();
/// <summary>
/// Get all registered modules
/// </summary>
public IEnumerable<IZeroModule> GetAll() => _modules.Values;
/// <summary>
/// Adds a zero module
/// </summary>
public void Add<T>() where T : class, IZeroModule, new()
{
Add(new T());
}
/// <summary>
/// Adds a zero module
/// </summary>
public void Add<T>(T moduleInstance) where T : IZeroModule
{
Add(typeof(T), moduleInstance);
}
/// <summary>
/// Adds a zero module
/// </summary>
public void Add(Type moduleType, IZeroModule moduleInstance)
{
if (_modules.ContainsKey(moduleType))
{
return;
}
_modules.TryAdd(moduleType, moduleInstance);
}
/// <inheritdoc />
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
foreach (var module in _modules.OrderBy(x => x.Value.Order))
{
module.Value.ConfigureServices(services, configuration);
}
}
/// <inheritdoc />
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
foreach (var module in _modules.OrderBy(x => x.Value.ConfigureOrder))
{
module.Value.Configure(app, routes, serviceProvider);
}
}
}
@@ -1,5 +0,0 @@
namespace zero.Communication;
public interface IHandler
{
}
@@ -1,32 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Communication;
public class HandlerHolder : IHandlerHolder
{
IServiceProvider Services { get; }
public HandlerHolder(IServiceProvider services)
{
Services = services;
}
public T Get<T>() where T : IHandler
{
return Services.GetService<T>();
}
public IEnumerable<T> GetAll<T>() where T : IHandler
{
return Services.GetService<IEnumerable<T>>();
}
}
public interface IHandlerHolder
{
T Get<T>() where T : IHandler;
IEnumerable<T> GetAll<T>() where T : IHandler;
}
@@ -1,207 +0,0 @@
namespace zero.Communication;
public abstract partial class Interceptor<T> : Interceptor, IInterceptor<T> where T : ZeroIdEntity
{
/// <inheritdoc />
public Type ModelType { get; protected set; }
public Interceptor()
{
Gravity = 0;
ModelType = typeof(T);
Name = ModelType.Name;
}
/// <inheritdoc />
public override bool CanHandle(InterceptorParameters args, Type modelType) => ModelType.IsAssignableFrom(modelType);
/// <inheritdoc />
public virtual Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
/// <inheritdoc />
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Creating(args, model as T));
/// <inheritdoc />
public virtual Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
/// <inheritdoc />
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Updating(args, model as T));
/// <inheritdoc />
public virtual Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
/// <inheritdoc />
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model) => Convert(await Deleting(args, model as T));
/// <inheritdoc />
public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask;
/// <inheritdoc />
public sealed override Task Created(InterceptorParameters args, ZeroIdEntity model) => Created(args, model as T);
/// <inheritdoc />
public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask;
/// <inheritdoc />
public sealed override Task Updated(InterceptorParameters args, ZeroIdEntity model) => Updated(args, model as T);
/// <inheritdoc />
public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask;
/// <inheritdoc />
public sealed override Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Deleted(args, model as T);
InterceptorResult<ZeroIdEntity> Convert(InterceptorResult<T> result)
{
if (result == default)
{
return default;
}
return new InterceptorResult<ZeroIdEntity>()
{
Continue = result.Continue,
InterceptorHash = result.InterceptorHash,
Result = result.Result != null ? result.Result.ConvertTo<ZeroIdEntity>(result.Result.Model) : null
};
}
}
public abstract partial class Interceptor : IInterceptor
{
/// <inheritdoc />
public int Gravity { get; protected set; }
/// <inheritdoc />
public string Name { get; protected set; }
/// <inheritdoc />
public string Hash { get; protected set; }
public Interceptor()
{
Gravity = 0;
Hash = IdGenerator.Create();
}
/// <inheritdoc />
public virtual bool CanHandle(InterceptorParameters args, Type modelType) => true;
/// <inheritdoc />
public virtual Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
/// <inheritdoc />
public virtual Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
/// <inheritdoc />
public virtual Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
/// <inheritdoc />
public virtual Task Created(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
/// <inheritdoc />
public virtual Task Updated(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
/// <inheritdoc />
public virtual Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
}
public interface IInterceptor<T> : IInterceptor where T : ZeroIdEntity
{
/// <summary>
/// Type of the associated model
/// </summary>
Type ModelType { get; }
/// <summary>
/// Called after an entity has been stored but before the session has saved its changes
/// </summary>
Task Created(InterceptorParameters args, T model);
/// <summary>
/// Called before an entity is stored and validated
/// </summary>
Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model);
/// <summary>
/// Called after an entity has been deleted but before the session has saved its changes
/// </summary>
Task Deleted(InterceptorParameters args, T model);
/// <summary>
/// Called before an entity is deleted
/// </summary>
Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model);
/// <summary>
/// Called after an entity has been updated but before the session has saved its changes
/// </summary>
Task Updated(InterceptorParameters args, T model);
/// <summary>
/// Called before an entity is stored and validated
/// </summary>
Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model);
}
public interface IInterceptor
{
/// <summary>
/// Interceptors with a higher gravity will run before any with lower gravity
/// </summary>
int Gravity { get; }
/// <summary>
/// Readable name for this interceptor
/// </summary>
string Name { get; }
/// <summary>
/// Hash which is used for this interceptor to be able to pass results from Start to Complete methods
/// </summary>
string Hash { get; }
/// <summary>
/// Whether any of the interceptor methods is allowed to run based on the parameters
/// </summary>
bool CanHandle(InterceptorParameters args, Type modelType);
/// <summary>
/// Called after an entity has been stored but before the session has saved its changes
/// </summary>
Task Created(InterceptorParameters args, ZeroIdEntity model);
/// <summary>
/// Called before an entity is stored and validated
/// </summary>
Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model);
/// <summary>
/// Called after an entity has been deleted but before the session has saved its changes
/// </summary>
Task Deleted(InterceptorParameters args, ZeroIdEntity model);
/// <summary>
/// Called before an entity is deleted
/// </summary>
Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model);
/// <summary>
/// Called after an entity has been updated but before the session has saved its changes
/// </summary>
Task Updated(InterceptorParameters args, ZeroIdEntity model);
/// <summary>
/// Called before an entity is stored and validated
/// </summary>
Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model);
}
@@ -1,148 +0,0 @@
using Microsoft.Extensions.Logging;
namespace zero.Communication;
public class InterceptorInstruction<T> where T : ZeroIdEntity, new()
{
public Guid Guid { get; private set; }
public InterceptorRunType Runtype { get; private set; }
public T Model { get; private set; }
public T PreviousModel { get; private set; }
public Type ModelType { get; private set; }
public Result<T> Result { get; private set; }
protected IZeroContext Context { get; private set; }
protected Lazy<IEnumerable<IInterceptor>> Interceptors { get; private set; }
protected Dictionary<IInterceptor, InterceptorParameters> InterceptorCache { get; private set; } = new();
protected IInterceptors InterceptorHandler { get; private set; }
protected ILogger<IInterceptor> Logger { get; private set; }
protected Func<IInterceptor, bool> InterceptorFilter { get; private set; } = x => true;
internal InterceptorInstruction(IInterceptors interceptors, IZeroContext context, Lazy<IEnumerable<IInterceptor>> registrations, ILogger<IInterceptor> logger, InterceptorRunType runtype, T model, T previousModel = null)
{
InterceptorHandler = interceptors;
Context = context;
Guid = Guid.NewGuid();
Logger = logger;
Runtype = runtype;
Model = model;
PreviousModel = previousModel;
Interceptors = registrations;
ModelType = model.GetType();
}
/// <summary>
/// Custom interceptor filter for this instruction (the CanHandle() method for each interceptor is still activated)
/// </summary>
public void Filter(Func<IInterceptor, bool> predicate)
{
InterceptorFilter = predicate;
}
/// <summary>
/// Run all interceptors (in order) which can handle the given type.
/// Depending on the action any of the following methods on the interceptor is called: Creating(), Updating(), Deleting().
/// If any of the interceptors returns a result the operation is cancelled and this result is returned.
/// </summary>
public async Task<bool> Start(IStoreOperations operations)
{
foreach (IInterceptor interceptor in GetInterceptors())
{
InterceptorParameters parameters = new()
{
Context = Context,
Store = Context.Store,
Properties = new(),
Interceptors = InterceptorHandler,
Operations = operations,
PreviousModel = PreviousModel
};
if (!interceptor.CanHandle(parameters, ModelType))
{
continue;
}
Logger.LogDebug("Run interceptor {interceptor} for {type}:{operation}", interceptor.Name, ModelType, Runtype);
InterceptorResult<ZeroIdEntity> result = (await HandleBefore(interceptor, parameters)) ?? new();
result.InterceptorHash = IdGenerator.Create(32);
InterceptorCache.Add(interceptor, parameters);
// we cancel all further interceptors if a result is available and return this instead
if (result.Result != null)
{
Result = result.Result.ConvertTo(result.Result.Model as T);
return false;
}
// the Continue task will cancel all further interceptors
if (!result.Continue)
{
break;
}
}
return true;
}
/// <summary>
/// Run all interceptors (in order) which can handle the given type.
/// Depending on the action any of the following methods on the interceptor is called: Created(), Updated(), Deleted().
/// The parameters which are returned from the Start() operation are passed to the methods.
/// </summary>
public async Task Complete()
{
foreach ((IInterceptor interceptor, InterceptorParameters parameters) in InterceptorCache)
{
await HandleAfter(interceptor, parameters);
}
}
protected IEnumerable<IInterceptor> GetInterceptors()
{
return Interceptors.Value.Where(InterceptorFilter).OrderByDescending(x => x.Gravity);
}
/// <summary>
/// Proxy for handling methods on an interceptor
/// </summary>
protected Task<InterceptorResult<ZeroIdEntity>> HandleBefore(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch
{
InterceptorRunType.Create => interceptor.Creating(parameters, Model),
InterceptorRunType.Update => interceptor.Updating(parameters, Model),
InterceptorRunType.Delete => interceptor.Deleting(parameters, Model),
_ => throw new NotImplementedException()
};
/// <summary>
/// Proxy for handling methods on an interceptor
/// </summary>
protected Task HandleAfter(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch
{
InterceptorRunType.Create => interceptor.Created(parameters, Model),
InterceptorRunType.Update => interceptor.Updated(parameters, Model),
InterceptorRunType.Delete => interceptor.Deleted(parameters, Model),
_ => throw new NotImplementedException()
};
}
@@ -1,44 +0,0 @@
namespace zero.Communication;
public class InterceptorParameters
{
/// <summary>
/// The current zero context
/// </summary>
public IZeroContext Context { get; set; }
/// <summary>
/// Raven document store
/// </summary>
public IZeroStore Store { get; set; }
/// <summary>
/// Access to other interceptor methods
/// </summary>
public IInterceptors Interceptors { get; internal set; }
/// <summary>
/// Access to operations
/// </summary>
public IStoreOperations Operations { get; internal set; }
/// <summary>
/// Parameters from the interceptor which ran on before the operation (only available for completed operations)
/// </summary>
public Dictionary<string, object> Properties { get; set; } = new();
/// <summary>
/// Get a typed property
/// </summary>
public TProp Property<TProp>(string key) => Properties.GetValueOrDefault<TProp>(key);
/// <summary>
/// Get a typed property
/// </summary>
public bool TryGetProperty<TProp>(string key, out TProp property) => Properties.TryGetValue(key, out property);
/// <summary>
/// Holds a reference to the previously existing model when an update happens (can be null)
/// </summary>
public object PreviousModel { get; set; }
}
@@ -1,19 +0,0 @@
namespace zero.Communication;
public class InterceptorResult<T>
{
/// <summary>
/// Autoset. Hash used to match interceptors in order to correctly pass parameters
/// </summary>
internal string InterceptorHash { get; set; }
/// <summary>
/// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...)
/// </summary>
public bool Continue { get; set; } = true;
/// <summary>
/// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors
/// </summary>
public Result<T> Result { get; set; }
}
@@ -1,8 +0,0 @@
namespace zero.Communication;
public enum InterceptorRunType
{
Create = 1,
Update = 2,
Delete = 3
}
@@ -1,49 +0,0 @@
using Microsoft.Extensions.Logging;
namespace zero.Communication;
public class Interceptors : IInterceptors
{
protected IZeroContext Context { get; set; }
protected Lazy<IEnumerable<IInterceptor>> Registrations { get; set; }
protected ILogger<IInterceptor> Logger { get; set; }
public Interceptors(IZeroContext context, Lazy<IEnumerable<IInterceptor>> registrations, ILogger<IInterceptor> logger)
{
Context = context;
Registrations = registrations;
Logger = logger;
}
/// <inheritdoc />
public InterceptorInstruction<T> ForCreate<T>(T model) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Create, model);
/// <inheritdoc />
public InterceptorInstruction<T> ForUpdate<T>(T model, T previousModel = null) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Update, model, previousModel);
/// <inheritdoc />
public InterceptorInstruction<T> ForDelete<T>(T model) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Delete, model);
}
public interface IInterceptors
{
/// <summary>
/// Instruction which can run interceptors before and after a creating an entity
/// </summary>
InterceptorInstruction<T> ForCreate<T>(T model) where T : ZeroIdEntity, new();
/// <summary>
/// Instruction which can run interceptors before and after updating an entity
/// </summary>
InterceptorInstruction<T> ForUpdate<T>(T model, T previousModel = null) where T : ZeroIdEntity, new();
/// <summary>
/// Instruction which can run interceptors before and after deleting an entity
/// </summary>
InterceptorInstruction<T> ForDelete<T>(T model) where T : ZeroIdEntity, new();
}
-11
View File
@@ -1,11 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Communication;
public class LazilyResolved<T> : Lazy<T>
{
public LazilyResolved(IServiceProvider serviceProvider): base(serviceProvider.GetRequiredService<T>)
{
}
}
@@ -1,6 +0,0 @@
namespace zero.Communication;
public interface IMessage
{
}
@@ -1,26 +0,0 @@
namespace zero.Communication;
/// <summary>
/// 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)
/// </summary>
public interface IMessageHandler<TMessage> where TMessage : IMessage
{
/// <summary>
/// Method to invoke when a message of type TMessage is published
/// </summary>
Task Handle(TMessage message);
}
/// <summary>
/// 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)
/// </summary>
public interface IBatchMessageHandler<TMessage> : IMessageHandler<TMessage> where TMessage : IMessage
{
/// <summary>
/// Method to invoke when a batch of messages of type TMessage are published
/// </summary>
Task HandleBatchAsync(IReadOnlyCollection<TMessage> message);
}
@@ -1,52 +0,0 @@
using System.Collections.Concurrent;
namespace zero.Communication;
public class MessageAggregator : IMessageAggregator
{
readonly ConcurrentBag<IMessageSubscription> Subscription = new ConcurrentBag<IMessageSubscription>();
readonly IServiceProvider ServiceProvider;
public MessageAggregator(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
/// <inheritdoc />
public async Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage
{
IEnumerable<IMessageSubscription> subscriptions = Subscription.Where(s => s.CanDeliver<TMessage>());
foreach (IMessageSubscription subscription in subscriptions)
{
await subscription.Deliver(ServiceProvider, message);
}
}
/// <inheritdoc />
public void Activate<TMessage, TMessageHandler>()
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>
{
Subscription.Add(new MessageSubscription<TMessage, TMessageHandler>());
}
}
public interface IMessageAggregator
{
/// <summary>
/// Publishes the specified message, invoking any handlers subscribed to the message.
/// </summary>
Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage;
/// <summary>
/// Subscribes the specified handler to the spified message type
/// </summary>
void Activate<TMessage, TMessageHandler>()
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>;
}
@@ -1,41 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace zero.Communication;
internal class MessageSubscription<TMessage, TMessageHandler> : IMessageSubscription
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>
{
public bool CanDeliver<T>()
{
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 handlers = serviceProvider.GetServices<TMessageHandler>();
foreach (var handler in handlers)
{
await handler.Handle((TMessage)message);
}
}
}
public interface IMessageSubscription
{
bool CanDeliver<TMessage>();
Task Deliver(IServiceProvider serviceProvider, object message);
}
@@ -1,15 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Communication;
internal class ZeroCommunicationModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IInterceptors, Interceptors>();
services.AddScoped<IMessageAggregator, MessageAggregator>();
services.AddTransient<IHandlerHolder, HandlerHolder>();
services.AddTransient(typeof(Lazy<>), typeof(LazilyResolved<>));
}
}
-102
View File
@@ -1,102 +0,0 @@
namespace zero.Configuration;
public static partial class Constants
{
public const string ErrorFieldNone = "__zero_no_field";
public static partial class Tabs
{
public const string General = "general";
}
public static partial class Auth
{
public const string SystemUser = "system";
public const string DefaultScheme = "zeroScheme";
public const string BackofficeDisplayName = "Zero Backoffice";
public const string BackofficeScheme = "zeroBackoffice";
public const string BackofficeCookieName = "zero.be.session";
public const string DefaultCookieName = "zero.session";
public static partial 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 partial class Permissions
{
public static partial class Groups
{
public const string Sections = "zero.permissions.groups.sections";
public const string Spaces = "zero.permissions.groups.spaces";
public const string Settings = "zero.permissions.groups.settings";
public const string Pages = "zero.permissions.groups.pages";
public const string Modules = "zero.permissions.groups.modules";
}
}
public static partial class Database
{
public const string ReservationPrefix = "zero.";
public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires;
}
public static partial 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 partial 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 partial class PermissionCollections
{
public const string Sections = "permissionCollectionSections";
public const string Spaces = "permissionCollectionSpaces";
public const string Settings = "permisssionCollectionSettings";
public const string Modules = "permissionCollectionModules";
}
public static partial class Pages
{
public const string FolderAlias = "zero.folder";
public const string DefaultRootPageTypeAlias = "root";
public const string PageRouteProviderAlias = "zero.pages";
}
public static partial class Routing
{
public const string InternalRoutePrefix = "/__zero/";
public const string ErrorRoute = InternalRoutePrefix + "error";
}
}
@@ -1,22 +0,0 @@
namespace zero.Configuration;
/// <summary>
/// A feature can affect both the backoffice and the frontend
/// </summary>
public class Feature
{
/// <summary>
/// The alias
/// </summary>
public string Alias { get; set; }
/// <summary>
/// The name of the feature
/// </summary>
public string Name { get; set; }
/// <summary>
/// Additional description
/// </summary>
public string Description { get; set; }
}
@@ -1,19 +0,0 @@
namespace zero.Configuration;
public class FeatureOptions : List<Feature>
{
public void Add<T>() where T : Feature, new()
{
Add(new T());
}
public void Add(string alias, string name, string description)
{
Add(new Feature()
{
Alias = alias,
Name = name,
Description = description
});
}
}
@@ -1,6 +0,0 @@
namespace zero.Configuration;
public interface IZeroCollectionOptions
{
}
@@ -1,21 +0,0 @@
using FluentValidation;
namespace zero.Configuration;
public static class FlavorOptionsExtensions
{
public static void AddIntegration<T>(this FlavorOptions options, string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration, new()
{
options.Add<Integration, T>(new IntegrationType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator,
Construct = cfg => new T(),
EditorAlias = editorAlias
});
}
}
@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace zero.Configuration;
/// <summary>
/// An integration is an application part which has a public configuration per app.
/// It's up to the user to provide functionality.
/// </summary>
[RavenCollection("Integrations")]
public class Integration : ZeroEntity
{
}
@@ -1,210 +0,0 @@
using FluentValidation;
using FluentValidation.Results;
using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
namespace zero.Configuration;
public class IntegrationStore : IIntegrationStore
{
protected IStoreOperations Operations { get; private set; }
protected IIntegrationTypeService IntegrationTypes { get; private set; }
public IntegrationStore(IStoreOperationsWithInactive operations, IIntegrationTypeService integrationTypes)
{
IntegrationTypes = integrationTypes;
Operations = operations;
}
/// <inheritdoc />
public virtual async Task<Integration> Empty(string alias)
{
return await Operations.Empty<Integration>(alias);
}
/// <inheritdoc />
public virtual async Task<Integration> Load(string alias)
{
Paged<Integration> result = await Operations.Load<Integration>(1, 1, q => q.Where(x => x.Flavor == alias));
return result.Items.FirstOrDefault();
}
/// <inheritdoc />
public async Task<IList<Integration>> LoadByTag(string tag)
{
IEnumerable<IntegrationType> types = IntegrationTypes.GetByTag(tag);
if (!types.Any())
{
return new List<Integration>();
}
string[] aliases = types.Select(x => x.Alias).ToArray();
return await Operations.Session.Query<Integration>().Where(x => x.Flavor.In(aliases)).ToListAsync();
}
/// <inheritdoc />
public async Task<bool> Any(string tag)
{
IEnumerable<IntegrationType> types = IntegrationTypes.GetByTag(tag);
if (!types.Any())
{
return false;
}
string[] aliases = types.Select(x => x.Alias).ToArray();
return await Operations.Session.Query<Integration>().AnyAsync(x => x.Flavor.In(aliases) && x.IsActive);
}
/// <inheritdoc />
public virtual string GetChangeToken(Integration model) => Operations.GetChangeToken(model);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Create(Integration model) => await Save(model, true);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Update(Integration model) => await Save(model, false);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Activate(string alias)
{
Integration model = await Load(alias);
if (model != null)
{
model.IsActive = true;
}
return await Update(model);
}
/// <inheritdoc />
public virtual async Task<Result<Integration>> Deactivate(string alias)
{
Integration model = await Load(alias);
if (model != null)
{
model.IsActive = false;
}
return await Update(model);
}
/// <inheritdoc />
public async Task<Result<Integration>> Delete(string alias)
{
Integration integration = await Load(alias);
return await Operations.Delete(integration);
}
// <inheritdoc />
protected virtual async Task<ValidationResult> Validate(Integration model, bool create = false)
{
ValidationResult result = new();
if (model == null)
{
result.Errors.Add(new ValidationFailure("__nofield", "@integration.errors.notfound"));
}
else
{
IntegrationType type = IntegrationTypes.GetByAlias(model.Flavor);
if (type == null)
{
result.Errors.Add(new ValidationFailure("__nofield", "@integration.errors.typenotfound"));
}
if (create && await Operations.Any<Integration>(q => q.Where(x => x.Flavor == model.Flavor)))
{
result.Errors.Add(new ValidationFailure("__nofield", "@integration.errors.multiplenotallowed"));
}
if (!create && await Operations.Any<Integration>(q => q.Where(x => x.Flavor == model.Flavor && x.Id != model.Id)))
{
result.Errors.Add(new ValidationFailure("__nofield", "@integration.errors.alreadycreated"));
}
if (type != null)
{
ValidationResult innerResult = await Operations.Validate(model);
result.Errors.AddRange(innerResult.Errors);
}
}
return result;
}
/// <inheritdoc />
protected virtual async Task<Result<Integration>> Save(Integration model, bool create = false)
{
return await Operations.Create(model, async x => await Validate(x, create));
}
}
public interface IIntegrationStore
{
/// <summary>
/// Get new instance of an integration by integration alias
/// </summary>
Task<Integration> Empty(string alias);
/// <summary>
/// Get an integration by integration alias
/// </summary>
Task<Integration> Load(string alias);
/// <summary>
/// Get all integrations by a certain tag
/// </summary>
Task<IList<Integration>> LoadByTag(string tag);
/// <summary>
/// Check if there are any activated integrations for a certain tag
/// </summary>
Task<bool> Any(string tag);
/// <summary>
/// Get the change vector for a model (Proxy to IAsyncDocumentSession.GetChangeVectorFor<>)
/// </summary>
string GetChangeToken(Integration model);
/// <summary>
/// Creates a new integration
/// </summary>
Task<Result<Integration>> Create(Integration model);
/// <summary>
/// Updates an integration
/// </summary>
Task<Result<Integration>> Update(Integration model);
/// <summary>
/// Activates a configured integration
/// </summary>
Task<Result<Integration>> Activate(string alias);
/// <summary>
/// Disables a configured integration
/// </summary>
Task<Result<Integration>> Deactivate(string alias);
/// <summary>
/// Deletes configuration of an integration and disables it
/// </summary>
Task<Result<Integration>> Delete(string alias);
}
@@ -1,34 +0,0 @@
using FluentValidation;
using System.Text.Json.Serialization;
namespace zero.Configuration;
/// <summary>
/// An integration is an application part which has a public configuration per app.
/// It's up to the user to provide functionality.
/// </summary>
public class IntegrationType : FlavorConfig
{
public IntegrationType(Type type) : base(type) { }
/// <summary>
/// Alias to find the editor schema
/// </summary>
public string EditorAlias { get; set; }
/// <summary>
/// Group integrations by tags
/// </summary>
public List<string> Tags { get; set; } = new();
/// <summary>
/// Image of the integration
/// </summary>
public string ImagePath { get; set; }
/// <summary>
/// Set a validator for this integration
/// </summary>
[JsonIgnore]
public IValidator Validator { get; set; }
}
@@ -1,70 +0,0 @@
namespace zero.Configuration;
public class IntegrationTypeService : IIntegrationTypeService
{
protected IZeroContext Context { get; private set; }
protected IHandlerHolder Handler { get; private set; }
protected FlavorOptions Flavors { get; set; }
public IntegrationTypeService(IZeroContext context, IZeroOptions options, IHandlerHolder handler)
{
Context = context;
Handler = handler;
Flavors = options.For<FlavorOptions>();
}
/// <inheritdoc />
public IEnumerable<IntegrationType> GetAll()
{
return Flavors.GetAll<Integration>().Select(x => (IntegrationType)x);
}
/// <inheritdoc />
public IntegrationType GetByAlias(string integrationTypeAlias)
{
return Flavors.Get<Integration>(integrationTypeAlias) as IntegrationType;
}
/// <inheritdoc />
public IntegrationType GetByType<T>() where T : Integration
{
return Flavors.Get<Integration, T>() as IntegrationType;
}
/// <inheritdoc />
public IEnumerable<IntegrationType> GetByTag(string tag)
{
return GetAll().Where(x => x.Tags.Contains(tag, StringComparer.InvariantCultureIgnoreCase));
}
}
public interface IIntegrationTypeService
{
/// <summary>
/// Get all available integration types
/// </summary>
IEnumerable<IntegrationType> GetAll();
/// <summary>
/// Get a specific integration type by alias
/// </summary>
IntegrationType GetByAlias(string integrationTypeAlias);
/// <summary>
/// Get a specific integration type by model type.
/// </summary>
IntegrationType GetByType<T>() where T : Integration;
/// <summary>
/// Get all integrations with a certain tag.
/// </summary>
IEnumerable<IntegrationType> GetByTag(string tag);
}
@@ -1,12 +0,0 @@
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; }
}
-26
View File
@@ -1,26 +0,0 @@
using FluentValidation;
namespace zero.Configuration;
public abstract class OptionsType
{
/// <summary>
/// Type of the associated model class
/// </summary>
public Type ContentType { get; set; }
/// <summary>
/// The alias
/// </summary>
public string Alias { get; set; }
/// <summary>
/// The name of the options type
/// </summary>
public string Name { get; set; }
/// <summary>
/// Set a validator for the editor
/// </summary>
public IValidator Validator { get; set; }
}
@@ -1,35 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace zero.Configuration;
internal class ZeroConfigurationModule : ZeroModule
{
public override int Order => -1000;
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<ZeroOptions>().Configure<IServiceProvider>((opts, svc) =>
{
opts.ServiceProvider = svc;
opts.Version = "0.0.1-alpha.1";
opts.ZeroPath = "/zero";
opts.TokenExpiration = TimeSpan.FromHours(3);
}).Bind(configuration.GetSection("Zero"));
services.AddTransient<IZeroOptions, ZeroOptions>(factory => factory.GetService<IOptions<ZeroOptions>>().Value);
services.AddScoped<IIntegrationTypeService, IntegrationTypeService>();
services.AddScoped<IIntegrationStore, IntegrationStore>();
services.AddOptions<FeatureOptions>().Bind(configuration.GetSection("Zero:Features"));
services.Configure<FlavorOptions>(opts =>
{
opts.Configure<Integration>(cfg =>
{
cfg.CanUseWithoutFlavors = false;
});
});
}
}
-79
View File
@@ -1,79 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
namespace zero.Configuration;
public class ZeroOptions : IZeroOptions
{
/// <inheritdoc />
public bool Initialized => !String.IsNullOrEmpty(For<RavenOptions>()?.Database);
/// <inheritdoc />
public string Version { get; set; }
/// <inheritdoc />
public string ZeroPath { get; set; }
/// <inheritdoc />
public TimeSpan TokenExpiration { get; set; }
/// <inheritdoc />
public List<ApplicationRegistration> Applications { get; set; } = new();
internal IServiceProvider ServiceProvider { get; set; }
protected ConcurrentDictionary<Type, object> OptionsCache { get; private set; } = new();
/// <inheritdoc />
public TOptions For<TOptions>() where TOptions : class
{
Type type = typeof(TOptions);
if (!OptionsCache.TryGetValue(type, out object value) && ServiceProvider != null)
{
IOptions<TOptions> options = ServiceProvider.GetService<IOptions<TOptions>>();
value = options.Value;
OptionsCache.TryAdd(type, value);
}
return value as TOptions;
}
}
public interface IZeroOptions
{
/// <summary>
/// Path to the backoffice. Defaults to /zero
/// </summary>
string ZeroPath { get; set; }
/// <summary>
/// The currently active version
/// This should not be set manually, as it is used for setup and migrations and incremented automatically
/// </summary>
string Version { get; set; }
/// <summary>
/// Whether this zero instance is initialized (setup is completed)
/// </summary>
bool Initialized { get; }
/// <summary>
/// Expiration of a generated change token for an entity
/// </summary>
TimeSpan TokenExpiration { get; set; }
/// <summary>
/// Contains all registered applications
/// </summary>
List<ApplicationRegistration> Applications { get; set; }
/// <summary>
/// Get typed options (proxy to IOptions<TOptions>)
/// </summary>
TOptions For<TOptions>() where TOptions : class;
}
@@ -1,23 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace zero.Configuration;
public class ZeroStartupOptions : IZeroStartupOptions
{
public IList<IAssemblyDiscoveryRule> AssemblyDiscoveryRules { get; private set; } = new List<IAssemblyDiscoveryRule>();
public IMvcBuilder Mvc { get; private set; }
public ZeroStartupOptions(IMvcBuilder mvc)
{
Mvc = mvc;
}
}
public interface IZeroStartupOptions
{
IList<IAssemblyDiscoveryRule> AssemblyDiscoveryRules { get; }
IMvcBuilder Mvc { get; }
}
-327
View File
@@ -1,327 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
namespace zero.Context;
public class ZeroContext : IZeroContext
{
/// <inheritdoc />
public Application Application { get; protected set; }
/// <inheritdoc />
public string AppId { get; protected set; }
/// <inheritdoc />
public ClaimsPrincipal BackofficeUser { get; protected set; }
/// <inheritdoc />
public bool IsBackofficeRequest { get; protected set; }
/// <inheritdoc />
public bool IsLoggedIntoBackoffice { get; protected set; }
/// <inheritdoc />
public IZeroOptions Options { get; protected set; }
/// <inheritdoc />
public Route Route => ResolvedRoute?.Route;
/// <inheritdoc />
public IRouteModel ResolvedRoute => HttpContextAccessor?.HttpContext?.Features.Get<IRouteModel>();
/// <inheritdoc />
public IZeroStore Store { get; private set; }
/// <inheritdoc />
public IServiceProvider Services { get; private set; }
/// <inheritdoc />
public ZeroContextScope Scope { get; private set; }
/// <inheritdoc />
public bool IsPreview { get; private set; }
protected IApplicationResolver AppResolver { get; private set; }
protected ICultureResolver CultureResolver { get; private set; }
protected ILogger<ZeroContext> Logger { get; private set; }
protected IHandlerHolder Handler { get; private set; }
protected IHttpContextAccessor HttpContextAccessor { get; private set; }
protected IPrimitiveTypeCollection ValueCollection { get; private set; }
protected IPreviewService PreviewService { get; private set; }
bool _resolved = false;
public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, IApplicationResolver appResolver, ICultureResolver cultureResolver,
ILogger<ZeroContext> logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services, IPreviewService previewService)
{
Options = options;
AppResolver = appResolver;
CultureResolver = cultureResolver;
Logger = logger;
Store = store;
Handler = handler;
ValueCollection = new PrimitiveTypeCollection();
HttpContextAccessor = httpContextAccessor;
Services = services;
PreviewService = previewService;
}
/// <inheritdoc />
public async virtual Task Resolve(HttpContext context)
{
if (_resolved)
{
return;
}
if (context?.Request is null)
{
Store.ResolvedDatabase = null;
return;
}
//if (!Options.SetupCompleted)
//{
// return;
//} // TODO setup
_resolved = true;
// check if the current request is a backoffice request
IsBackofficeRequest = context.IsBackofficeRequest(Options.ZeroPath);
// 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;
}
// check whether the current request is a preview request
IsPreview = await PreviewService.IsAccessGranted(context.Request, BackofficeUser);
// resolve current application
Application = await AppResolver.Resolve(context, BackofficeUser);
AppId = Application.Id;
Logger.LogDebug("Resolved {appId} ({uri})", AppId, context.Request.Host.ToString() + context.Request.Path.Value.EnsureStartsWith('/'));
// set default database for document store
Store.ResolvedDatabase = Application.Database;
// set current culture
await CultureResolver.Resolve(this);
// set context scope
Scope = new(Store, Store.ResolvedDatabase, Application);
}
/// <inheritdoc />
public T Get<T>() => ValueCollection.Get<T>();
/// <inheritdoc />
public void Set<T>(T value) => ValueCollection.Set(value);
/// <inheritdoc />
public void Remove<T>() => ValueCollection.Remove<T>();
/// <inheritdoc />
public void SetScope(Application app)
{
// TODO this is only used for console app as we do not have an HttpRequest there
// needs to be deleted and definitely replaced
_resolved = true;
IsBackofficeRequest = false;
BackofficeUser = new ClaimsPrincipal();
IsLoggedIntoBackoffice = false;
Application = app;
AppId = app.Id;
Store.ResolvedDatabase = Application.Database;
Scope = new(Store, Store.ResolvedDatabase, Application);
}
/// <inheritdoc />
public ZeroContextScope CreateScope(Application app)
{
ApplyScope(app.Database, app);
return new ZeroContextScope(Store, app.Database, app, Scope, scope =>
{
Scope = scope.Previous;
ApplyScope(Scope.Database, Scope.Application);
});
}
/// <inheritdoc />
public ZeroContextScope CreateScope(string database)
{
ApplyScope(database);
return new ZeroContextScope(Store, database, null, Scope, scope =>
{
Scope = scope.Previous;
ApplyScope(Scope.Database, Scope.Application);
});
}
/// <summary>
/// Apply a database scope
/// </summary>
void ApplyScope(string database, Application app = null)
{
Application = app;
AppId = app?.Id;
Store.ResolvedDatabase = database;
}
}
public class ZeroContextScope : IDisposable
{
public ZeroContextScope(IZeroStore store, string database, Application application, ZeroContextScope previous = null, Action<ZeroContextScope> onDispose = null)
{
Store = store;
Database = database;
Application = application;
Previous = previous;
_onDispose = onDispose;
}
public IZeroStore Store { get; set; }
public Application Application { get; private set; }
public string Database { get; private set; }
public ZeroContextScope Previous { get; private set; }
Action<ZeroContextScope> _onDispose = null;
public void Dispose()
{
_onDispose?.Invoke(this);
}
}
public interface IZeroContext
{
/// <summary>
/// Currently loaded application
/// </summary>
Application Application { get; }
/// <summary>
/// Current loaded application Id
/// </summary>
string AppId { get; }
/// <summary>
/// Resolved backoffice user principal
/// </summary>
ClaimsPrincipal BackofficeUser { get; }
/// <summary>
/// Whether the current request is a backoffice request or not
/// </summary>
bool IsBackofficeRequest { get; }
/// <summary>
/// Whether the user is logged into the backoffice
/// </summary>
bool IsLoggedIntoBackoffice { get; }
/// <summary>
/// Determines whether the current request is a preview request
/// </summary>
bool IsPreview { get; }
/// <summary>
/// Global zero options
/// </summary>
IZeroOptions Options { get; }
/// <summary>
/// Document store
/// </summary>
IZeroStore Store { get; }
/// <summary>
/// Service container
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Current context scope
/// </summary>
ZeroContextScope Scope { get; }
/// <summary>
/// Matching (frontend) path route
/// </summary>
Route Route { get; }
/// <summary>
/// Matching (frontend) resolved route
/// </summary>
IRouteModel ResolvedRoute { get; }
/// <summary>
/// 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
/// </summary>
Task Resolve(HttpContext context);
/// <summary>
/// Get a custom property from this scoped context
/// </summary>
T Get<T>();
/// <summary>
/// Add a custom property to this scoped context
/// </summary>
void Set<T>(T value);
/// <summary>
/// Remove a custom property from this scoped context
/// </summary>
void Remove<T>();
/// <summary>
/// Manually set a scope
/// </summary>
void SetScope(Application app);
/// <summary>
/// Scope the current context to a specific application database
/// </summary>
ZeroContextScope CreateScope(Application app);
/// <summary>
/// Scope the current context to a specific database
/// </summary>
ZeroContextScope CreateScope(string database);
}
@@ -1,20 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace zero.Context
{
public class ZeroContextMiddleware
{
RequestDelegate Next;
public ZeroContextMiddleware(RequestDelegate next)
{
Next = next;
}
public async Task Invoke(HttpContext httpContext, IZeroContext zeroContext)
{
await zeroContext.Resolve(httpContext);
await Next(httpContext);
}
}
}
-13
View File
@@ -1,13 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Context;
internal class ZeroContextModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IZeroContext, ZeroContext>();
services.AddHttpContextAccessor();
}
}
-57
View File
@@ -1,57 +0,0 @@
namespace zero.Extensions;
public static class CharExtensions
{
private static Dictionary<char, char[]> accents { get; } = new Dictionary<char, char[]>()
{
{ 'ä', new char[2] { 'a', 'e' } },
{ 'á', new char[1] { 'a' } },
{ 'à', new char[1] { 'a' } },
{ 'ó', new char[1] { 'o' } },
{ 'ò', new char[1] { 'o' } },
{ 'é', new char[1] { 'e' } },
{ 'è', new char[1] { 'e' } },
{ 'ú', new char[1] { 'u' } },
{ 'ù', new char[1] { 'u' } },
{ 'í', new char[1] { 'i' } },
{ 'ì', new char[1] { 'i' } },
{ 'ö', new char[2] { 'o', 'e' } },
{ 'ü', new char[2] { 'u', 'e' } },
{ 'ß', new char[2] { 's', 's' } },
{ '&', new char[1] { '+' } }
};
/// <summary>
/// Check if a character is from a-z, A-Z or 0-9
/// </summary>
public static bool IsAZor09(this char value)
{
return (value >= 0x41 && value <= 0x5A) || (value >= 0x61 && value <= 0x7a) || (value >= 0x30 && value <= 0x39);
}
/// <summary>
/// Check if a character is in ASCII range
/// </summary>
public static bool IsASCII(this char value)
{
return value < 128;
}
/// <summary>
/// Replaces an accent or umlaut with the appropriate URL + file ready variant
/// </summary>
public static bool TryReplaceAccent(this char value, out char[] result)
{
if (!accents.ContainsKey(value))
{
result = null;
return false;
}
result = accents[value];
return true;
}
}
-58
View File
@@ -1,58 +0,0 @@
using System.Drawing;
namespace zero.Extensions;
public static class ColorExtensions
{
/// <summary>
/// Get color object from a hex string
/// </summary>
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)
);
}
/// <summary>
/// Get contrast ratio between two color values
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L95" />
/// </summary>
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);
}
/// <summary>
/// Get luminance value of a color based on WCAG-conform JS implementation
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L42" />
/// </summary>
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;
}
}
@@ -1,36 +0,0 @@
namespace zero.Extensions;
public static class DictionaryExtensions
{
public static bool TryGetValue<T>(this Dictionary<string, object> model, string key, out T value)
{
if (!model.TryGetValue(key, out object valueObj) || !(valueObj is T))
{
value = default;
return false;
}
value = (T)valueObj;
return true;
}
public static T GetValueOrDefault<T>(this Dictionary<string, object> model, string key)
{
object value = model.GetValueOrDefault(key);
return value == default || !(value is T) ? default : (T)value;
}
public static Dictionary<TKey, TElement> ToDistinctDictionary<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector)
{
Dictionary<TKey, TElement> result = new();
foreach (TSource sourceElement in source)
{
result.TryAdd(keySelector(sourceElement), elementSelector(sourceElement));
}
return result;
}
}
@@ -1,36 +0,0 @@
namespace zero.Extensions;
public static class EnumerableExtensions
{
public static IEnumerable<T> Paging<T>(this IEnumerable<T> 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<T>(this IEnumerable<T> source, Func<T, bool> predicate, out T model)
{
model = source.FirstOrDefault(predicate);
return model != null;
}
public static bool TryAdd<T>(this IList<T> source, T model) where T : class
{
if (model == default)
{
return false;
}
source.Add(model);
return true;
}
}
@@ -1,48 +0,0 @@
using System.Linq.Expressions;
using System.Text;
namespace zero.Extensions;
public static class ExpressionExtensions
{
internal static string GetPropertyPath(this Expression expr, out MemberExpression member)
{
StringBuilder path = new();
MemberExpression memberExpression = GetMemberExpression(expr);
do
{
member = memberExpression;
if (path.Length > 0)
{
path.Insert(0, ".");
}
path.Insert(0, memberExpression.Member.Name);
memberExpression = GetMemberExpression(memberExpression.Expression);
}
while (memberExpression != null);
return path.ToString();
}
internal static MemberExpression GetMemberExpression(this Expression expression)
{
if (expression is MemberExpression)
{
return (MemberExpression)expression;
}
else if (expression is LambdaExpression)
{
var lambdaExpression = expression as LambdaExpression;
if (lambdaExpression.Body is MemberExpression)
{
return (MemberExpression)lambdaExpression.Body;
}
else if (lambdaExpression.Body is UnaryExpression)
{
return ((MemberExpression)((UnaryExpression)lambdaExpression.Body).Operand);
}
}
return null;
}
}
@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace zero.Extensions;
public static class HttpContextExtensions
{
/// <summary>
/// Whether the current request is a backoffice request
/// </summary>
public static bool IsBackofficeRequest(this HttpContext context, string backofficePath)
{
string path = backofficePath.EnsureStartsWith('/').TrimEnd('/');
return context.Request.Path.ToString().StartsWith(path);
}
/// <summary>
/// Whether the current request is an AJAX request
/// </summary>
public static bool IsAjaxRequest(this HttpContext context)
{
if (context?.Request?.Headers == null)
{
return false;
}
return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}
/// <summary>
/// Whether the current request only accepts application/json
/// </summary>
public static bool IsJsonRequest(this HttpContext context)
{
if (context?.Request?.Headers == null)
{
return false;
}
return context.Request.Headers["Accept"] == "application/json";
}
/// <summary>
/// Get IP Address of the client
/// </summary>
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");
}
}
-47
View File
@@ -1,47 +0,0 @@
namespace zero.Extensions;
public static class NumberExtensions
{
static Dictionary<FileSizeNotation, string[]> 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]);
}
}
-30
View File
@@ -1,30 +0,0 @@
//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<T>(this Type type)
// {
// return type.IsAssignableFrom(typeof(T));
// }
// public static bool Is<T>(this object obj)
// {
// return obj.GetType().IsAssignableFrom(typeof(T));
// }
// public static T Clone<T>(this T obj)
// {
// Type type = obj.GetType();
// return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type);
// }
// public static T CloneLax<T>(this object obj)
// {
// return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
// }
//}
-48
View File
@@ -1,48 +0,0 @@
namespace zero.Extensions;
public static class ResultExtensions
{
public static Result WithoutModel<TSource>(this Result<TSource> origin)
{
Result result = new();
result.IsSuccess = origin.IsSuccess;
result.Errors = origin.Errors;
return result;
}
public static Result<TTarget> ConvertTo<TTarget>(this Result origin, TTarget model)
{
Result<TTarget> result = new();
result.IsSuccess = origin.IsSuccess;
result.Errors = origin.Errors;
result.Model = model;
return result;
}
public static Result AddError(this Result origin, string property, string message)
{
origin.IsSuccess = false;
origin.Errors.Add(new(property, message));
return origin;
}
public static Result AddError(this Result origin, string message)
{
origin.IsSuccess = false;
origin.Errors.Add(new(Constants.ErrorFieldNone, message));
return origin;
}
public static Result AppendErrors(this Result origin, Result with)
{
foreach (ResultError error in with.Errors)
{
origin.Errors.Add(error);
}
origin.IsSuccess = origin.Errors.Any();
return origin;
}
}
@@ -1,109 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Reflection;
namespace zero.Extensions;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds all found implementations based on the service type and assembly discovery rules
/// </summary>
public static void AddAll<TService>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false) => services.AddAll(typeof(TService), lifetime, asSelf);
/// <summary>
/// Adds all found implementations based on the service type and assembly discovery rules
/// </summary>
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<Type> 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));
}
}
}
/// <summary>
/// Adds or overrides an implementation
/// </summary>
public static void Replace<TService, TImplementation>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient)
where TService : class
where TImplementation : class, TService
{
services.RemoveAll<TService>();
services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime));
}
/// <summary>
/// Adds or overrides an implementation
/// </summary>
public static void Replace<TService, TImplementation>(this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory, ServiceLifetime lifetime = ServiceLifetime.Transient)
where TService : class
where TImplementation : class, TService
{
services.RemoveAll<TService>();
services.Add(new ServiceDescriptor(typeof(TService), implementationFactory, lifetime));
}
/// <summary>
/// Adds or overrides an implementation
/// </summary>
public static void Replace<TService, TOldImplementation, TNewImplementation>(this IServiceCollection services)
where TService : class
where TOldImplementation : class, TService
where TNewImplementation : class, TService
{
ServiceDescriptor oldDescriptor = services.FirstOrDefault(x => x.ImplementationType == typeof(TOldImplementation));
if (oldDescriptor != null)
{
services.Remove(oldDescriptor);
services.Add(new ServiceDescriptor(typeof(TService), typeof(TNewImplementation), oldDescriptor.Lifetime));
}
}
/// <summary>
/// Removes an implementation
/// </summary>
public static void Remove<TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
ServiceDescriptor oldDescriptor = services.FirstOrDefault(x => x.ImplementationType == typeof(TImplementation));
if (oldDescriptor != null)
{
services.Remove(oldDescriptor);
}
}
}
-196
View File
@@ -1,196 +0,0 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace zero.Extensions;
public static class StringExtensions
{
const string SPACE = " ";
static Regex replaceMultipleSpacesRegex { get; } = new Regex("[ ]{2,}", RegexOptions.None);
static Regex newLineCharsRegex { get; } = new Regex("\t|\n|\r", RegexOptions.None);
public static string FullTrim(this string value)
{
if (String.IsNullOrEmpty(value))
{
return value;
}
return replaceMultipleSpacesRegex.Replace(value, SPACE).Trim();
}
public static string EnsureStartsWith(this string input, string toStartWith)
{
if (input.StartsWith(toStartWith)) return input;
return toStartWith + input.TrimStart(toStartWith);
}
public static string EnsureStartsWith(this string input, char value)
{
return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input;
}
public static string EnsureEndsWith(this string input, char value)
{
return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value;
}
public static string EnsureEndsWith(this string input, string toEndWith)
{
return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith;
}
public static string EnsureSurroundedWith(this string input, char toSurroundWith)
{
return input.EnsureStartsWith(toSurroundWith).EnsureEndsWith(toSurroundWith);
}
public static string Or(this string value, string fallback)
{
return String.IsNullOrWhiteSpace(value) ? fallback : value;
}
public static string TrimEnd(this string value, string forRemoving)
{
if (String.IsNullOrEmpty(value)) return value;
if (String.IsNullOrEmpty(forRemoving)) return value;
while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
{
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
}
return value;
}
public static string TrimStart(this string value, string forRemoving)
{
if (String.IsNullOrEmpty(value)) return value;
if (String.IsNullOrEmpty(forRemoving)) return value;
while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
{
value = value.Substring(forRemoving.Length);
}
return value;
}
public static bool HasValue(this string input)
{
return !String.IsNullOrWhiteSpace(input);
}
public static bool IsNullOrEmpty(this string input)
{
return String.IsNullOrEmpty(input);
}
public static bool IsNullOrWhiteSpace(this string input)
{
return String.IsNullOrWhiteSpace(input);
}
public static string ToCamelCase(this string input)
{
if (!String.IsNullOrEmpty(input) && input.Length > 1)
{
return Char.ToLowerInvariant(input[0]) + input.Substring(1);
}
return input;
}
public static string ToPascalCase(this string input)
{
if (!String.IsNullOrEmpty(input) && input.Length > 1)
{
return Char.ToUpperInvariant(input[0]) + input.Substring(1);
}
return input;
}
public static string ToCamelCaseId(this string input)
{
if (String.IsNullOrEmpty(input))
{
return input;
}
if (input.Length < 2)
{
return input.ToLowerInvariant();
}
string[] parts = input.Split('.');
return String.Join(".", parts.Select(x => x.ToCamelCase()));
}
public static string ToPascalCaseId(this string input)
{
if (String.IsNullOrEmpty(input))
{
return input;
}
if (input.Length < 2)
{
return input.ToUpperInvariant();
}
string[] parts = input.Split('.');
return String.Join(".", parts.Select(x => x.ToPascalCase()));
}
public static string RemoveNewLines(this string input)
{
if (String.IsNullOrEmpty(input))
{
return input;
}
return newLineCharsRegex.Replace(input, String.Empty).Trim();
}
public static string Shorten(this string input, int maxLength)
{
if (maxLength < 4)
{
throw new ArgumentOutOfRangeException("maxLength", "Has to be at least 4 characters long");
}
if (String.IsNullOrEmpty(input) || input.Length <= maxLength)
{
return input;
}
return input.Substring(0, maxLength - 3) + "...";
}
public static string ReplaceFirst(this string text, string search, string replace)
{
int pos = text.IndexOf(search);
if (pos < 0)
{
return text;
}
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
}
}
-10
View File
@@ -1,10 +0,0 @@
namespace zero.FileStorage;
public class FileResult
{
public string Filename { get; set; }
public string Path { get; set; }
public string ContentType { get; set; }
}
-18
View File
@@ -1,18 +0,0 @@
namespace zero.FileStorage;
public enum FileSizeNotation
{
/// <summary>
/// Multiply by 1.000, as specified by the International System of Units (e.g. kB, MB, GB)
/// </summary>
SI = 0,
/// <summary>
/// Binary notation with a multiplier of 1.024 (e.g. KiB, MiB, GiB)
/// </summary>
IEC = 1,
/// <summary>
/// Binary notation with a multiplier of 1.024 (e.g. KB, MB, GB).
/// Ends with GB, there is no TB, ...
/// </summary>
JEDEC = 2
}
@@ -1,10 +0,0 @@
namespace zero.FileStorage;
public class FileSystemException : Exception
{
public FileSystemException() { }
public FileSystemException(string message) : base(message) { }
public FileSystemException(string message, Exception innerException) : base(message, innerException) { }
}
@@ -1,6 +0,0 @@
namespace zero.FileStorage;
public class FileSystemOptions
{
public string ZeroAssetsPath { get; set; }
}
-44
View File
@@ -1,44 +0,0 @@
namespace zero.FileStorage;
public interface IFileMeta
{
/// <summary>
/// Full path of the item within the file system
/// </summary>
string Path { get; }
/// <summary>
/// Absolute path of the item
/// </summary>
string AbsolutePath { get; }
/// <summary>
/// Name of the item
/// </summary>
string Name { get; }
/// <summary>
/// Full path to the directory containing the item
/// </summary>
string DirectoryPath { get; }
/// <summary>
/// Length of the file in bytes
/// </summary>
long Length { get; }
/// <summary>
/// Date when the item was last modified
/// </summary>
DateTimeOffset LastModifiedDate { get; }
/// <summary>
/// Whether this item is directory or a file
/// </summary>
bool IsDirectory { get; }
/// <summary>
/// Additional properties
/// </summary>
Dictionary<string, object> Properties { get; }
}
-71
View File
@@ -1,71 +0,0 @@
using System.IO;
namespace zero.FileStorage;
public interface IFileSystem
{
/// <summary>
/// Map a relative path to an absolute internal path
/// </summary>
string Map(string path);
/// <summary>
/// Map an internal path to a public accessible path
/// </summary>
string MapToPublicPath(string path);
/// <summary>
/// Determines whether a file or directory at the given path already exists
/// </summary>
Task<bool> Exists(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Get information of a file
/// </summary>
Task<IFileMeta> GetFileInfo(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Returns a readable stream for a file
/// </summary>
Task<Stream> StreamFile(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a file at the given path
/// </summary>
Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a file at the given path. This method returns if the file does not exist.
/// </summary>
Task DeleteFile(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Moves a file to another path (can also be used to rename resources)
/// </summary>
Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default);
/// <summary>
/// Copies a file to another path
/// </summary>
Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default);
/// <summary>
/// Get information of a directory
/// </summary>
Task<IFileMeta> GetDirectoryInfo(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Get all items within a directory
/// </summary>
IAsyncEnumerable<IFileMeta> GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to create a directory at the given path. This method returns if the directory already exists.
/// </summary>
Task CreateDirectory(string path, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a directory at the given path. This method returns if the directory does not exist.
/// </summary>
Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default);
}
-217
View File
@@ -1,217 +0,0 @@
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; }
public const string MEDIA_FOLDER = "Uploads";
public const char PATH_SEPARATOR = '/';
static char[] InvalidFilenameChars = null;
const char REPLACEMENT_CHAR = '-';
FileExtensionContentTypeProvider FileExtensionContentTypeProvider { get; set; }
static readonly Dictionary<char, string> Replacements = new()
{
{ 'ä', "ae" },
{ 'ü', "ue" },
{ 'ö', "oe" },
{ 'ß', "ss" }
};
public Paths(IWebHostEnvironment env)
{
Env = env;
WebRoot = env.WebRootPath;
ContentRoot = env.ContentRootPath;
SecureRoot = Path.Combine(ContentRoot, "wwwroot.secure");
Media = Path.Combine(WebRoot, MEDIA_FOLDER);
FileExtensionContentTypeProvider = new();
}
/// <summary>
/// Combine a path
/// </summary>
public string Combine(params string[] paths)
{
return Path.Combine(paths);
}
/// <summary>
/// Map a path
/// </summary>
public string Map(string path)
{
return Path.Combine(WebRoot, path);
}
/// <summary>
/// Map a secure path
/// </summary>
public string MapSecure(string path)
{
return Path.Combine(SecureRoot, path);
}
/// <summary>
/// Map a path
/// </summary>
public string Map(params string[] paths)
{
return Path.Combine(WebRoot, Path.Combine(paths));
}
/// <summary>
/// Map a secure path
/// </summary>
public string MapSecure(params string[] paths)
{
return Path.Combine(SecureRoot, Path.Combine(paths));
}
/// <summary>
/// Create a directory if it does not exist yet
/// </summary>
public void Create(string directory)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
/// <summary>
/// Get content type for a filename
/// </summary>
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;
}
/// <summary>
/// Normalizes a filename and removes invalid chars
/// </summary>
public string ToFilename(string value)
{
if (String.IsNullOrWhiteSpace(value))
{
return value;
}
// lowercase for string
value = value.ToLower();
StringBuilder sb = new(value.Length);
var invalids = 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; }
/// <summary>
/// Combine a path
/// </summary>
string Combine(params string[] paths);
/// <summary>
/// Map a path
/// </summary>
string Map(string path);
/// <summary>
/// Map a secure path
/// </summary>
string MapSecure(string path);
/// <summary>
/// Map a path
/// </summary>
string Map(params string[] paths);
/// <summary>
/// Map a secure path
/// </summary>
string MapSecure(params string[] paths);
/// <summary>
/// Create a directory if it does not exist yet
/// </summary>
void Create(string directory);
/// <summary>
/// Get content type for a filename
/// </summary>
string GetContentType(string filename, string fallback = "application/octet-stream");
/// <summary>
/// Normalizes a filename and removes invalid chars
/// </summary>
string ToFilename(string value);
}
-43
View File
@@ -1,43 +0,0 @@
using Microsoft.Extensions.FileProviders;
namespace zero.FileStorage;
public class PhysicalFileMeta : IFileMeta
{
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public string Path { get; }
/// <inheritdoc />
public string AbsolutePath { get; }
/// <inheritdoc />
public string DirectoryPath { get; }
/// <inheritdoc />
public long Length { get; }
/// <inheritdoc />
public DateTimeOffset LastModifiedDate { get; }
/// <inheritdoc />
public bool IsDirectory { get; }
/// <inheritdoc />
public Dictionary<string, object> Properties { get; }
public PhysicalFileMeta(IFileInfo fileInfo, string path)
{
Path = path;
AbsolutePath = fileInfo.PhysicalPath;
Name = fileInfo.Name;
DirectoryPath = path.Substring(0, path.Length - fileInfo.Name.Length).TrimEnd('/');
LastModifiedDate = fileInfo.LastModified;
Length = fileInfo.Length;
IsDirectory = fileInfo.IsDirectory;
Properties = new();
}
}
-305
View File
@@ -1,305 +0,0 @@
using Microsoft.Extensions.FileProviders.Physical;
using System.IO;
namespace zero.FileStorage;
public class PhysicalFileSystem : IFileSystem
{
readonly string _root;
public PhysicalFileSystem(string root)
{
_root = root;
}
/// <inheritdoc />
public virtual string Map(string path)
{
return ResolvePath(path);
}
/// <inheritdoc />
public virtual string MapToPublicPath(string path)
{
return path.EnsureStartsWith('/');
}
/// <inheritdoc />
public virtual Task<bool> Exists(string path, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
return Task.FromResult(File.Exists(resolvedPath) || Directory.Exists(resolvedPath));
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not get file exists info for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task<IFileMeta> GetFileInfo(string path, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
PhysicalFileInfo fileInfo = new(new FileInfo(resolvedPath));
if (!fileInfo.Exists)
{
return Task.FromResult<IFileMeta>(default);
}
return Task.FromResult<IFileMeta>(new PhysicalFileMeta(fileInfo, path));
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not get file info for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task<Stream> StreamFile(string path, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
if (resolvedPath.IsNullOrEmpty() || !File.Exists(resolvedPath))
{
throw new FileSystemException($"The path '{path}' does not exist in the file system");
}
return Task.FromResult<Stream>(File.OpenRead(resolvedPath));
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not get file stream for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual async Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
if ((!overwrite && File.Exists(resolvedPath)) || Directory.Exists(resolvedPath))
{
throw new FileSystemException($"A file/directory at the path '{path}' already existsin the file system");
}
string directoryPath = Path.GetDirectoryName(resolvedPath);
Directory.CreateDirectory(directoryPath);
FileInfo fileInfo = new(resolvedPath);
using var outputStream = fileInfo.Create();
await fileStream.CopyToAsync(outputStream, cancellationToken);
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not get file stream for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task DeleteFile(string path, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
File.Delete(resolvedPath);
return Task.CompletedTask;
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not delete file for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default)
{
try
{
string resolvedOldPath = ResolvePath(oldPath);
string resolvedNewPath = ResolvePath(newPath);
if (!File.Exists(resolvedOldPath))
{
throw new FileSystemException($"The path '{oldPath}' does not exist in the file system");
}
if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath))
{
throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system");
}
File.Move(resolvedOldPath, resolvedNewPath);
return Task.CompletedTask;
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not move file from path '{oldPath}' to '{newPath}'", ex);
}
}
/// <inheritdoc />
public virtual Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default)
{
try
{
string resolvedOldPath = ResolvePath(oldPath);
string resolvedNewPath = ResolvePath(newPath);
if (!File.Exists(resolvedOldPath))
{
throw new FileSystemException($"The path '{oldPath}' does not exist in the file system");
}
if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath))
{
throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system");
}
File.Copy(resolvedOldPath, resolvedNewPath);
return Task.CompletedTask;
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not copy file from path '{oldPath}' to '{newPath}'", ex);
}
}
/// <inheritdoc />
public virtual Task<IFileMeta> GetDirectoryInfo(string path, CancellationToken cancellationToken = default) => GetFileInfo(path, cancellationToken);
/// <inheritdoc />
public virtual IAsyncEnumerable<IFileMeta> GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
List<IFileMeta> results = new();
SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
if (!Directory.Exists(resolvedPath))
{
return results.ToAsyncEnumerable();
}
results.AddRange(Directory.GetDirectories(resolvedPath, "*", searchOption).Select(f =>
{
PhysicalDirectoryInfo fileSystemInfo = new(new DirectoryInfo(f));
return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length)));
}));
results.AddRange(Directory.GetFiles(resolvedPath, "*", searchOption).Select(f =>
{
PhysicalFileInfo fileSystemInfo = new(new FileInfo(f));
return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length)));
}));
return results.ToAsyncEnumerable();
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not get directory content for path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task CreateDirectory(string path, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
if (File.Exists(resolvedPath))
{
throw new FileSystemException($"A file at the path '{path}' already exists in the file system");
}
if (!Directory.Exists(resolvedPath))
{
Directory.CreateDirectory(resolvedPath);
}
return Task.CompletedTask;
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not create directory at path '{path}'", ex);
}
}
/// <inheritdoc />
public virtual Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default)
{
try
{
string resolvedPath = ResolvePath(path);
Directory.Delete(resolvedPath, recursive);
return Task.CompletedTask;
}
catch (Exception ex) when (ex is not FileSystemException)
{
throw new FileSystemException($"Could not delete directory at path '{path}'", ex);
}
}
/// <summary>
/// Creates a fully-usable absolte path from the given path
/// </summary>
protected virtual string ResolvePath(string path)
{
try
{
if (path.IsNullOrWhiteSpace())
{
return _root;
}
// normalize path
path = path.Replace('\\', '/').FullTrim().Trim('/');
string physicalPath = Path.Combine(_root, path);
string rootPath = Path.GetFullPath(_root);
// do not allow paths which are outside this file system
// the boundaries are set be the basePath which is assigned in the file system constructor
if (!Path.GetFullPath(physicalPath).StartsWith(rootPath, StringComparison.InvariantCultureIgnoreCase))
{
throw new FileSystemException($"The path '{path}' is outside the file system");
}
return physicalPath;
}
catch (FileSystemException)
{
throw;
}
catch (Exception ex)
{
throw new FileSystemException($"Could not resolve path '{path}'", ex);
}
}
}
@@ -1,28 +0,0 @@
namespace zero.Media;
public class WebRootFileSystem : PhysicalFileSystem, IWebRootFileSystem
{
protected string PublicPathPrefix { get; }
public WebRootFileSystem(string root, string publicPathPrefix) : base(root)
{
PublicPathPrefix = (publicPathPrefix ?? String.Empty).EnsureEndsWith('/');
}
/// <inheritdoc />
public override string MapToPublicPath(string path)
{
if (path.IsNullOrEmpty())
{
return null;
}
return PublicPathPrefix + path.TrimStart('/');
}
}
public interface IWebRootFileSystem : IFileSystem
{
}
@@ -1,25 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace zero.FileStorage;
internal class ZeroFileStorageModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IPaths>(factory => new Paths(factory.GetService<IWebHostEnvironment>()));
services.AddOptions<FileSystemOptions>().Bind(configuration.GetSection("Zero:FileSystem")).Configure(opts =>
{
opts.ZeroAssetsPath = "zero";
});
services.AddSingleton<IWebRootFileSystem, WebRootFileSystem>(svc =>
{
IWebHostEnvironment env = svc.GetRequiredService<IWebHostEnvironment>();
return new(env.WebRootPath, null);
});
}
}
@@ -1,95 +0,0 @@
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<TUser, TRole>(this AuthenticationBuilder builder, Action<OptionsBuilder<CookieAuthenticationOptions>> setupAction = null)
where TUser : ZeroUser
where TRole : ZeroUserRole
{
return builder.AddCookie<TUser>(Constants.Auth.BackofficeScheme, Constants.Auth.BackofficeDisplayName, true, b =>
{
b.Configure<IZeroOptions>((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.ZeroPath.EnsureStartsWith('/').TrimEnd('/'));
});
options.Events.OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
setupAction?.Invoke(b);
});
}
public static AuthenticationBuilder AddZeroCookie<TUser, TRole>(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action<OptionsBuilder<CookieAuthenticationOptions>> setupAction = null)
where TUser : ZeroIdentityUser
where TRole : ZeroIdentityRole
{
return builder.AddCookie<TUser>(scheme, cookieDisplayName, false, setupAction);
}
public static AuthenticationBuilder AddZeroCookie<TUser>(this AuthenticationBuilder builder, string scheme, string cookieDisplayName = null, Action<OptionsBuilder<CookieAuthenticationOptions>> setupAction = null)
where TUser : ZeroIdentityUser
{
return builder.AddCookie<TUser>(scheme, cookieDisplayName, false, setupAction);
}
static AuthenticationBuilder AddCookie<TUser>(this AuthenticationBuilder builder, string scheme, string displayName, bool isBackoffice, Action<OptionsBuilder<CookieAuthenticationOptions>> setupAction = null)
where TUser : ZeroIdentityUser
{
IServiceCollection services = builder.Services;
services
.AddOptions<ZeroAuthOptions<TUser>>()
.Configure<IZeroOptions>((options, zero) =>
{
options.Scheme = scheme;
options.Path = isBackoffice ? zero.ZeroPath : "/";
});
var optionsBuilder = services
.AddOptions<CookieAuthenticationOptions>(scheme)
.Configure<IOptionsMonitor<ZeroAuthOptions<TUser>>>((options, monitor) =>
{
ZeroAuthOptions<TUser> opts = monitor.CurrentValue;
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;
});
setupAction?.Invoke(optionsBuilder);
builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(scheme, displayName, null);
return builder;
}
}
@@ -1,21 +0,0 @@
namespace zero.Identity;
public static class BackofficeUserExtensions
{
public static string[] GetAllowedAppIds(this ZeroUser user)
{
if (user == null)
{
return Array.Empty<string>();
}
return Array.Empty<string>();
// TODO permissions
//IEnumerable<Permission> 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();
}
}
-116
View File
@@ -1,116 +0,0 @@
using System.Text;
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
// see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/Base32.cs
public static class Base32
{
private static readonly string _base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public static string ToBase32(byte[] input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
StringBuilder sb = new StringBuilder();
for (int offset = 0; offset < input.Length;)
{
byte a, b, c, d, e, f, g, h;
int numCharsToOutput = GetNextGroup(input, ref offset, out a, out b, out c, out d, out e, out f, out g, out h);
sb.Append((numCharsToOutput >= 1) ? _base32Chars[a] : '=');
sb.Append((numCharsToOutput >= 2) ? _base32Chars[b] : '=');
sb.Append((numCharsToOutput >= 3) ? _base32Chars[c] : '=');
sb.Append((numCharsToOutput >= 4) ? _base32Chars[d] : '=');
sb.Append((numCharsToOutput >= 5) ? _base32Chars[e] : '=');
sb.Append((numCharsToOutput >= 6) ? _base32Chars[f] : '=');
sb.Append((numCharsToOutput >= 7) ? _base32Chars[g] : '=');
sb.Append((numCharsToOutput >= 8) ? _base32Chars[h] : '=');
}
return sb.ToString();
}
public static byte[] FromBase32(string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
input = input.TrimEnd('=').ToUpperInvariant();
if (input.Length == 0)
{
return new byte[0];
}
var output = new byte[input.Length * 5 / 8];
var bitIndex = 0;
var inputIndex = 0;
var outputBits = 0;
var outputIndex = 0;
while (outputIndex < output.Length)
{
var byteIndex = _base32Chars.IndexOf(input[inputIndex]);
if (byteIndex < 0)
{
throw new FormatException();
}
var bits = Math.Min(5 - bitIndex, 8 - outputBits);
output[outputIndex] <<= bits;
output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits)));
bitIndex += bits;
if (bitIndex >= 5)
{
inputIndex++;
bitIndex = 0;
}
outputBits += bits;
if (outputBits >= 8)
{
outputIndex++;
outputBits = 0;
}
}
return output;
}
// returns the number of bytes that were output
private static int GetNextGroup(byte[] input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)
{
uint b1, b2, b3, b4, b5;
int retVal;
switch (input.Length - offset)
{
case 1: retVal = 2; break;
case 2: retVal = 4; break;
case 3: retVal = 5; break;
case 4: retVal = 7; break;
default: retVal = 8; break;
}
b1 = (offset < input.Length) ? input[offset++] : 0U;
b2 = (offset < input.Length) ? input[offset++] : 0U;
b3 = (offset < input.Length) ? input[offset++] : 0U;
b4 = (offset < input.Length) ? input[offset++] : 0U;
b5 = (offset < input.Length) ? input[offset++] : 0U;
a = (byte)(b1 >> 3);
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
c = (byte)((b2 >> 1) & 0x1f);
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
f = (byte)((b4 >> 2) & 0x1f);
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
h = (byte)(b5 & 0x1f);
return retVal;
}
}
-16
View File
@@ -1,16 +0,0 @@
namespace zero.Identity.Models;
public enum LoginResult
{
[Localize("@login.errors.unknown")]
Unknown = 0,
[Localize("@login.errors.wrongcredentials")]
WrongCredentials = 1,
[Localize("@login.errors.lockedout")]
LockedOut = 2,
[Localize("@login.errors.notallowed")]
NotAllowed = 3,
[Localize("@login.errors.requirestwofactor")]
RequiresTwoFactor = 4,
Success = 10
}
-41
View File
@@ -1,41 +0,0 @@
using System.Security.Claims;
namespace zero.Identity;
public class UserClaim
{
/// <summary>
/// Gets or sets the claim type for this claim
/// </summary>
public string Type { get; set; }
/// <summary>
/// Gets or sets the claim value for this claim
/// </summary>
public string Value { get; set; }
/// <summary>
/// Convert to a claim
/// </summary>
public Claim ToClaim() => new(Type, Value);
public UserClaim() { }
public UserClaim(string type, string value)
{
Type = type;
Value = value;
}
public UserClaim(string type, string key, string value)
{
Type = type;
Value = key + ":" + value;
}
public UserClaim(Claim claim)
{
Type = claim?.Type;
Value = claim?.Value;
}
}
@@ -1,14 +0,0 @@
namespace zero.Identity;
public class UserClaimComparer : IEqualityComparer<UserClaim>
{
public bool Equals(UserClaim x, UserClaim y)
{
return (x == null && y == null) || (x.Type.Equals(y.Type, StringComparison.InvariantCultureIgnoreCase) && x.Value.Equals(y.Value, StringComparison.InvariantCultureIgnoreCase));
}
public int GetHashCode(UserClaim obj)
{
return (obj.Type + obj.Value).GetHashCode();
}
}
@@ -1,9 +0,0 @@
namespace zero.Identity;
public abstract class ZeroIdentityRole : ZeroEntity
{
/// <summary>
/// The role's claims, for use in claims-based authentication.
/// </summary>
public List<UserClaim> Claims { get; set; } = new();
}
@@ -1,73 +0,0 @@
namespace zero.Identity;
public abstract class ZeroIdentityUser : ZeroEntity
{
/// <summary>
/// Optional username (can also be used as login when configured)
/// </summary>
public string Username { get; set; }
/// <summary>
/// E-Mail address which is also used as the username
/// </summary>
public string Email { get; set; }
/// <summary>
/// Whether the email address has been confirmed
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// The password hash
/// </summary>
public string PasswordHash { get; set; }
/// <summary>
/// The security stamp
/// </summary>
public string SecurityStamp { get; set; }
/// <summary>
/// The user's claims, for use in claims-based authentication.
/// </summary>
public List<UserClaim> Claims { get; set; } = new();
/// <summary>
/// The roles (aliases) of the user
/// </summary>
public List<string> RoleIds { get; set; } = new();
/// <summary>
/// Number of times sign in failed.
/// </summary>
public int AccessFailedCount { get; set; }
/// <summary>
/// Whether the user is locked out.
/// </summary>
public bool LockoutEnabled { get; set; }
/// <summary>
/// When the user lock out is over.
/// </summary>
public DateTimeOffset? LockoutEnd { get; set; }
///// <summary>
///// Whether 2-factor authentication is enabled or not
///// </summary>
//public bool TwoFactorEnabled { get; set; }
///// <summary>
///// The two-factor authenticator key
///// </summary>
//public string TwoFactorAuthenticatorKey { get; set; }
///// <summary>
///// The list of two factor authentication recovery codes
///// </summary>
//public List<string> TwoFactorRecoveryCodes { get; set; }
}
-28
View File
@@ -1,28 +0,0 @@
namespace zero.Identity;
/// <summary>
/// A zero user can access the zero API and backoffice by granting the necessary permissions
/// </summary>
[RavenCollection("Users")]
public class ZeroUser : ZeroIdentityUser
{
/// <summary>
/// Application the user registered in
/// </summary>
public string AppId { get; set; }
/// <summary>
/// Currently selected app id for the backoffice
/// </summary>
public string CurrentAppId { get; set; }
/// <summary>
/// Super user
/// </summary>
public bool IsSuper { get; set; }
/// <summary>
/// Avatar image
/// </summary>
public string AvatarId { get; set; }
}
-15
View File
@@ -1,15 +0,0 @@
namespace zero.Identity;
[RavenCollection("UserRoles")]
public class ZeroUserRole : ZeroIdentityRole
{
/// <summary>
/// Additional description
/// </summary>
public string Description { get; set; }
/// <summary>
/// Displayed icon alongside name
/// </summary>
public string Icon { get; set; }
}
-21
View File
@@ -1,21 +0,0 @@
namespace zero.Identity;
public class PasswordGenerator
{
const string CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*";
private static Random random = new();
/// <summary>
/// Create a new password
/// </summary>
public static string Random(int length = -1)
{
if (length < 1)
{
length = 12;
}
return new string(Enumerable.Repeat(CHARS, length).Select(s => s[random.Next(s.Length)]).ToArray());
}
}
@@ -1,39 +0,0 @@
namespace zero.Identity;
public class EntityPermission
{
/// <summary>
/// Whether an entity can be read
/// </summary>
public bool CanRead { get; set; }
/// <summary>
/// Whether an entity can be created
/// </summary>
public bool CanCreate { get; set; }
/// <summary>
/// Whether an entity can be created in the shared app space
/// </summary>
public bool CanCreateShared { get; set; }
/// <summary>
/// Whether an entity can be edited or only viewed
/// </summary>
public bool CanEdit { get; set; }
/// <summary>
/// Whether an entity can be deleted
/// </summary>
public bool CanDelete { get; set; }
/// <summary>
/// Wehther an entity is application aware
/// </summary>
public bool IsAppAware { get; set; }
/// <summary>
/// Whether an entity can be shared across applications (only for IsAppAware=true)
/// </summary>
public bool IsShareable { get; set; }
}
@@ -1,85 +0,0 @@
using System.Security.Claims;
namespace zero.Identity;
public struct Permission
{
/// <summary>
/// Full key (stored in left part claim value) of the permission
/// </summary>
public string Key { get; set; }
/// <summary>
/// Title of this permission for output
/// </summary>
public string Label { get; set; }
/// <summary>
/// When a permission is disabled it can not be updated anymore
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// Child permissions
/// </summary>
public HashSet<Permission> Children { get; set; }
public Permission(string key, string label)
{
Key = key;
Label = label;
IsDisabled = false;
Children = new();
}
///// <summary>
///// Whether a resource (with authorization based on the key) can be read by any of the given permisssions
///// </summary>
//public static bool CanReadKey(IEnumerable<Permission> permissions, string key, bool isNormalized = false)
//{
// Permission permission = permissions.FirstOrDefault(p => isNormalized ? p.NormalizedKey == key : p.Key == key);
// return permission?.CanRead ?? false;
//}
///// <summary>
///// Whether a resource (with authorization based on the key) can be written to by any of the given permisssions
///// </summary>
//public static bool CanWriteKey(IEnumerable<Permission> permissions, string key, bool isNormalized = false)
//{
// Permission permission = permissions.FirstOrDefault(p => isNormalized ? p.NormalizedKey == key : p.Key == key);
// return permission?.CanWrite ?? false;
//}
///// <summary>
///// Create a permission from a claim
///// </summary>
//public static Permission FromClaim(Claim claim, string prefixToRemove = null)
//{
// if (claim == null)
// {
// return null;
// }
// return FromClaim(claim.Value, prefixToRemove);
//}
///// <summary>
///// Create a permission from a claim
///// </summary>
//public static Permission FromClaim(string claimValue, string prefixToRemove = null)
//{
// Permission permission = new Permission();
// string[] valueParts = claimValue.Split(':');
// permission.Key = valueParts[0];
// permission.NormalizedKey = permission.Key;
// permission.Value = valueParts.Length > 1 ? valueParts[1] : null;
// if (!prefixToRemove.IsNullOrEmpty())
// {
// permission.NormalizedKey = valueParts[0].TrimStart(prefixToRemove);
// }
// return permission;
//}
}
@@ -1,80 +0,0 @@
namespace zero.Identity;
public class PermissionContext : IPermissionContext
{
/// <inheritdoc />
public HashSet<PermissionGroup> Groups { get; protected set; }
/// <inheritdoc />
public IZeroOptions Options { get; protected set; }
public PermissionContext(IZeroOptions options)
{
Groups = new();
Options = options;
}
/// <inheritdoc />
public PermissionGroup? GetGroup(string key)
{
return Groups.FirstOrDefault(x => x.Key == key);
}
/// <inheritdoc />
public bool TryGetGroup(string key, out PermissionGroup group)
{
group = Groups.FirstOrDefault(x => x.Key == key);
return Groups.Contains(group);
}
/// <inheritdoc />
public void AddGroup(PermissionGroup group)
{
Groups.Add(group);
}
/// <inheritdoc />
public void RemoveGroup(PermissionGroup group)
{
Groups.Remove(group);
}
}
public interface IPermissionContext
{
/// <summary>
/// All registered permission groups
/// </summary>
HashSet<PermissionGroup> Groups { get; }
/// <summary>
/// Zero options
/// </summary>
IZeroOptions Options { get; }
/// <summary>
/// Try to get a permission group by key
/// </summary>
PermissionGroup? GetGroup(string key);
/// <summary>
/// Get permission group by key
/// </summary>
bool TryGetGroup(string key, out PermissionGroup group);
/// <summary>
/// Add a new permission group to the context
/// </summary>
void AddGroup(PermissionGroup group);
/// <summary>
/// Remove a permission group from the context
/// </summary>
void RemoveGroup(PermissionGroup group);
}
@@ -1,63 +0,0 @@
namespace zero.Identity;
public struct PermissionGroup
{
/// <summary>
/// Full key of the group
/// </summary>
public string Key { get; set; }
/// <summary>
/// Title of this permission group for output
/// </summary>
public string Label { get; set; }
/// <summary>
/// Permissions for this group
/// </summary>
public HashSet<Permission> Permissions { get; set; }
public PermissionGroup(string key, string label)
{
Key = key;
Label = label;
Permissions = new();
}
/// <summary>
/// Add a new permission to this group
/// </summary>
public void Add(Permission permission)
{
Permissions.Add(permission);
}
/// <summary>
/// Remove a permission
/// </summary>
public void Remove(Permission permission)
{
Permissions.Remove(permission);
}
/// <summary>
/// Get permission by key
/// </summary>
public Permission? Get(string key)
{
return Permissions.FirstOrDefault(x => x.Key == key);
}
/// <summary>
/// Get permission by key
/// </summary>
public object this[string key]
{
get => Permissions.FirstOrDefault(x => x.Key == key);
}
}
@@ -1,21 +0,0 @@
namespace zero.Identity;
public abstract class PermissionProvider : IPermissionProvider
{
/// <inheritdoc />
public virtual Task Configure(IPermissionContext context) => Task.CompletedTask;
}
public interface IPermissionProvider
{
/// <summary>
/// Manage permissions (add/remove/update permission groups and containing permissions)
/// </summary>
Task Configure(IPermissionContext context);
/// <summary>
/// In order for this permissions to work, the specified provider requires the following permission
/// </summary>
//IEnumerable<string> Requires() => Array.Empty<string>();
}
@@ -1,67 +0,0 @@
namespace zero.Identity;
public struct Permissions
{
/// <summary>
/// Ability to switch zero applications and read/write other applications out of the user appId
/// If PermissionsValue.Write is set for this permission, the user will be limited to his/her claims in other applications too
/// </summary>
public const string Applications = "applications.";
public struct Settings
{
public const string PREFIX = "settings.area.";
public const string Updates = PREFIX + Constants.Settings.Updates;
public const string Applications = PREFIX + Constants.Settings.Applications;
public const string Users = PREFIX + Constants.Settings.Users;
public const string Languages = PREFIX + Constants.Settings.Languages;
public const string Translations = PREFIX + Constants.Settings.Translations;
public const string Countries = PREFIX + Constants.Settings.Countries;
public const string Mails = PREFIX + Constants.Settings.Mails;
public const string Logging = PREFIX + Constants.Settings.Logging;
public const string Plugins = PREFIX + Constants.Settings.Plugins;
public const string CreatePlugin = PREFIX + Constants.Settings.CreatePlugin;
}
public struct Sections
{
public const string PREFIX = "section.";
public const string Dashboard = PREFIX + Constants.Sections.Dashboard;
public const string Pages = PREFIX + Constants.Sections.Pages;
public const string Spaces = PREFIX + Constants.Sections.Spaces;
public const string Media = PREFIX + Constants.Sections.Media;
public const string Settings = PREFIX + Constants.Sections.Settings;
}
public struct Spaces
{
public const string PREFIX = "space.";
}
public struct Modules
{
public const string PREFIX = "module.";
}
}
public struct PermissionsValue
{
public const string Create = "create";
public const string Read = "read";
public const string Update = "update";
public const string Delete = "delete";
public const string None = "none";
public const string True = "true";
public const string False = "false";
}
-150
View File
@@ -1,150 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
using Raven.Client.Exceptions;
using System.Security.Claims;
namespace zero.Identity;
public class RavenRoleStore<TRole> :
EntityStore<TRole>,
IRoleStore<TRole>,
IRoleClaimStore<TRole>
where TRole : ZeroIdentityRole, new()
{
protected IdentityErrorDescriber ErrorDescriber { get; private set; }
protected bool Global { get; private set; }
public RavenRoleStore(IStoreContext storeContext, IdentityErrorDescriber describer = null, bool global = false) : base(storeContext)
{
Global = global;
Config.Database = global ? Options.For<RavenOptions>().Database : null;
Config.IncludeInactive = true;
ErrorDescriber = describer ?? new IdentityErrorDescriber();
}
// <summary>
/// Scope queries to an optional application or something else
/// </summary>
protected virtual IRavenQueryable<TRole> ScopeQuery(IRavenQueryable<TRole> query) => query;
/// <inheritdoc/>
public async Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken)
{
await Create(role);
return IdentityResult.Success;
}
/// <inheritdoc/>
public async Task<IdentityResult> UpdateAsync(TRole role, CancellationToken cancellationToken)
{
try
{
await Update(role);
}
catch (ConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <inheritdoc/>
public async Task<IdentityResult> DeleteAsync(TRole role, CancellationToken cancellationToken)
{
try
{
await Delete(role);
}
catch (ConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <inheritdoc/>
public Task<string> GetRoleIdAsync(TRole role, CancellationToken cancellationToken) => Task.FromResult(role.Id);
/// <inheritdoc/>
public Task<string> GetRoleNameAsync(TRole role, CancellationToken cancellationToken) => Task.FromResult(role.Name);
/// <inheritdoc/>
public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken)
{
role.Name = roleName;
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<string> GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken) => Task.FromResult(role.Name);
/// <inheritdoc/>
public async Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken)
{
await SetRoleNameAsync(role, normalizedName, cancellationToken);
}
/// <inheritdoc/>
public async Task<TRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
{
return await ScopeQuery(Session.Query<TRole>()).FirstOrDefaultAsync(x => x.Id == roleId, cancellationToken);
}
/// <inheritdoc/>
public async Task<TRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
return await ScopeQuery(Session.Query<TRole>()).FirstOrDefaultAsync(x => x.Name == normalizedRoleName, cancellationToken);
}
/// <inheritdoc/>
public void Dispose()
{
}
/*
* ****************************************************
* CLAIM
* ****************************************************
*/
/// <inheritdoc/>
public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default)
{
role.Claims.Add(new(claim));
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<IList<Claim>> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default)
{
return Task.FromResult((IList<Claim>)role.Claims.Select(claim => claim.ToClaim()).ToList());
}
/// <inheritdoc/>
public Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default)
{
UserClaim userClaim = new(claim);
role.Claims = role.Claims.Except(new List<UserClaim>() { userClaim }, new UserClaimComparer()).ToList();
return Task.CompletedTask;
}
}

Some files were not shown because too many files have changed in this diff Show More