remove legacy project
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
public static IZeroApplicationBuilder UseZero(this IApplicationBuilder app) => new ZeroApplicationBuilder(app);
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<>));
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
// }
|
||||
//}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user