RE 1
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Identity;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
[ZeroAuthorize]
|
||||
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
|
||||
[ApiController]
|
||||
[Route("getsreplaced/[controller]/[action]")]
|
||||
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
|
||||
public abstract class ZeroBackofficeController : ControllerBase
|
||||
{
|
||||
IZeroOptions _options;
|
||||
|
||||
public bool IsCoreDatabase { get; protected set; }
|
||||
|
||||
protected IZeroOptions Options => _options ?? (_options = HttpContext?.RequestServices?.GetService<IZeroOptions>());
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is execuated when the scope changes.
|
||||
/// The scope is evaluated by the BackofficeFilterAttribute.
|
||||
/// </summary>
|
||||
public virtual void OnScopeChanged(string scope) { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates an edit model with appropriate options and permissions
|
||||
/// </summary>
|
||||
public EditModel<T> Edit<T>(T data, bool typed = true, Action<EditModel<T>> transform = null) where T : ZeroIdEntity
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
//ControllerContext.ActionDescriptor.FilterDescriptors[0].
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EditModel<T> model = new EditModel<T>();
|
||||
model.Entity = data;
|
||||
model.Meta.Token = null; // Token.Get(data);
|
||||
//model.Meta.IsAppAware = AppAwareType.IsAssignableFrom(type); // TODO appx
|
||||
//model.Meta.CanBeShared = canBeShared;
|
||||
model.Meta.CanCreate = true;
|
||||
//model.Meta.CanCreateShared = canBeShared;
|
||||
model.Meta.CanEdit = true;
|
||||
model.Meta.CanDelete = true;
|
||||
model.Meta.IsShared = IsCoreDatabase;
|
||||
|
||||
transform?.Invoke(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates an edit model with appropriate options and permissions
|
||||
/// </summary>
|
||||
public TWrapper Edit<T, TWrapper>(TWrapper data, bool typed = true, Action<EditModel<T>> transform = null)
|
||||
where T : ZeroIdEntity
|
||||
where TWrapper : EditModel<T>, new()
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
//ControllerContext.ActionDescriptor.FilterDescriptors[0].
|
||||
|
||||
data.Meta.Token = null; // Token.Get(data.Entity);
|
||||
//data.Meta.IsAppAware = AppAwareType.IsAssignableFrom(type); // TODO appx
|
||||
//data.Meta.CanBeShared = canBeShared;
|
||||
data.Meta.CanCreate = true;
|
||||
//data.Meta.CanCreateShared = canBeShared;
|
||||
data.Meta.CanEdit = true;
|
||||
data.Meta.CanDelete = true;
|
||||
data.Meta.IsShared = IsCoreDatabase;
|
||||
|
||||
transform?.Invoke(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
public IList<PreviewModel> Previews<T>(Dictionary<string, T> items, Func<T, PreviewModel> transform)
|
||||
{
|
||||
IList<PreviewModel> previews = new List<PreviewModel>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
bool exists = item.Value != null;
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
previews.Add(new PreviewModel()
|
||||
{
|
||||
HasError = true,
|
||||
Icon = "fth-alert-circle color-red",
|
||||
Id = item.Key,
|
||||
Name = "@errors.preview.notfound",
|
||||
Text = "@errors.preview.notfound_text"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
previews.Add(transform(item.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
|
||||
public IList<PreviewModel> Previews<T>(Dictionary<string, T> items, Action<T, PreviewModel> transform = null) where T : ZeroIdEntity
|
||||
{
|
||||
IList<PreviewModel> previews = new List<PreviewModel>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Value == null)
|
||||
{
|
||||
previews.Add(new PreviewModel()
|
||||
{
|
||||
HasError = true,
|
||||
Icon = "fth-alert-circle color-red",
|
||||
Id = item.Key,
|
||||
Name = "@errors.preview.notfound",
|
||||
Text = "@errors.preview.notfound_text"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewModel model = new()
|
||||
{
|
||||
Id = item.Value.Id
|
||||
};
|
||||
|
||||
if (item.Value is ZeroEntity)
|
||||
{
|
||||
model.Name = (item.Value as ZeroEntity).Name;
|
||||
}
|
||||
|
||||
transform?.Invoke(item.Value, model);
|
||||
|
||||
previews.Add(model);
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<IList<SelectModel>> SelectList<T>(IAsyncEnumerable<T> enumerable, Action<T, SelectModel> transform = null) where T : ZeroIdEntity
|
||||
{
|
||||
List<SelectModel> items = new List<SelectModel>();
|
||||
|
||||
await foreach (T item in enumerable)
|
||||
{
|
||||
SelectModel model = new()
|
||||
{
|
||||
Id = item.Id
|
||||
};
|
||||
|
||||
if (item is ZeroEntity)
|
||||
{
|
||||
model.Name = (item as ZeroEntity).Name;
|
||||
model.IsActive = (item as ZeroEntity).IsActive;
|
||||
}
|
||||
|
||||
transform?.Invoke(item, model);
|
||||
|
||||
items.Add(model);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<IList<PreviewModel>> Previews<T>(Dictionary<string, T> items, Func<T, Task<PreviewModel>> transform)
|
||||
{
|
||||
IList<PreviewModel> previews = new List<PreviewModel>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
bool exists = item.Value != null;
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
previews.Add(new PreviewModel()
|
||||
{
|
||||
HasError = true,
|
||||
Icon = "fth-alert-circle color-red",
|
||||
Id = item.Key,
|
||||
Name = "@errors.preview.notfound",
|
||||
Text = "@errors.preview.notfound_text"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
previews.Add(await transform(item.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
|
||||
protected IActionResult Download(Stream stream, string filename)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
// TODO add success property + return error response
|
||||
}
|
||||
|
||||
if (filename.Contains("{date}"))
|
||||
{
|
||||
filename = filename.Replace("{date}", DateTimeOffset.Now.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
if (filename == null || !provider.TryGetContentType(Path.GetFileName(filename), out string contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
Response.Headers.Add("zero-filename", filename);
|
||||
|
||||
return base.File(stream, contentType, filename, true);
|
||||
}
|
||||
|
||||
|
||||
protected IActionResult File(Core.Entities.FileResult file)
|
||||
{
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
FileStream stream = System.IO.File.OpenRead(file.Path);
|
||||
return File(stream, file.ContentType, file.Filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Raven.Client.Documents.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core;
|
||||
using zero.Core.Entities;
|
||||
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
public abstract class ZeroBackofficeCollectionController<TEntity, TCollection> : ZeroBackofficeController
|
||||
where TEntity : ZeroIdEntity, new()
|
||||
where TCollection : IEntityCollection<TEntity>
|
||||
{
|
||||
protected TCollection Collection { get; private set; }
|
||||
|
||||
[Obsolete]
|
||||
protected Func<IRavenQueryable<TEntity>, IRavenQueryable<TEntity>> DefaultQuery { get; set; }
|
||||
|
||||
protected Action<TEntity, PreviewModel> PreviewTransform { get; set; }
|
||||
|
||||
protected Action<TEntity, SelectModel> PickerTransform { get; set; }
|
||||
|
||||
|
||||
public ZeroBackofficeCollectionController(TCollection collection)
|
||||
{
|
||||
Collection = collection;
|
||||
}
|
||||
|
||||
public override void OnScopeChanged(string scope)
|
||||
{
|
||||
//Collection.ApplyScope(scope);
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<EditModel<TEntity>> GetById([FromQuery] string id, [FromQuery] string changeVector = null) => Edit(await Collection.Load(id, changeVector));
|
||||
|
||||
|
||||
public virtual async Task<Dictionary<string, TEntity>> GetByIds([FromQuery] string[] ids) => await Collection.Load(ids);
|
||||
|
||||
|
||||
public virtual async Task<EditModel<TEntity>> GetEmpty() => Edit(await Collection.Empty());
|
||||
|
||||
|
||||
public virtual async Task<ListResult<TEntity>> GetByQuery([FromQuery] ListBackofficeQuery<TEntity> query)
|
||||
{
|
||||
return await Collection.Load(query);
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<ListResult<Revision>> GetRevisions([FromQuery] string id, [FromQuery] ListBackofficeQuery<TEntity> query)
|
||||
{
|
||||
return null; // TODO
|
||||
//return await Collection.GetRevisions(id, query.Page, query.PageSize);
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<IEnumerable<SelectModel>> GetForPicker() => await SelectList(Collection.Stream(), PickerTransform);
|
||||
|
||||
|
||||
public virtual async Task<IList<PreviewModel>> GetPreviews([FromQuery] List<string> ids) => Previews(await Collection.Load(ids), PreviewTransform);
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public virtual async Task<EntityResult<TEntity>> Save([FromBody] TEntity model) => await Collection.Save(model);
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
public virtual async Task<EntityResult<TEntity>> Delete([FromQuery] string id) => await Collection.Delete(id);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//using Microsoft.AspNetCore.Mvc;
|
||||
//using Raven.Client.Documents;
|
||||
//using Raven.Client.Documents.Linq;
|
||||
//using System.Threading.Tasks;
|
||||
//using zero;
|
||||
|
||||
//namespace zero.Backoffice
|
||||
//{
|
||||
// [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)]
|
||||
// public class CountriesController : ZeroBackofficeCollectionController<Country, ICountriesCollection>
|
||||
// {
|
||||
// public CountriesController(ICountriesCollection collection) : base(collection)
|
||||
// {
|
||||
// PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant();
|
||||
// }
|
||||
|
||||
// public override Task<ListResult<Country>> GetByQuery([FromQuery] ListBackofficeQuery<Country> query)
|
||||
// {
|
||||
// query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
|
||||
// return Collection.Load<zero_Countries>(query);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
public class EditModel : EditModel<object> { }
|
||||
|
||||
public class EditModel<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Model
|
||||
/// </summary>
|
||||
public T Entity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Meta data
|
||||
/// </summary>
|
||||
public EditMetaModel Meta { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public class EditMetaModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created
|
||||
/// </summary>
|
||||
public bool CanCreate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created in the shared app space
|
||||
/// </summary>
|
||||
public bool CanCreateShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be edited or only viewed
|
||||
/// </summary>
|
||||
public bool CanEdit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be deleted
|
||||
/// </summary>
|
||||
public bool CanDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wehther this entity is application aware
|
||||
/// </summary>
|
||||
public bool IsAppAware { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be shared across applications (only for IsAppAware=true)
|
||||
/// </summary>
|
||||
public bool CanBeShared { get; set; }
|
||||
|
||||
public bool IsShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The change token maps to a database entity which holds ID and collection of the model to edit
|
||||
/// If these values do not match the entity on save it is rejected
|
||||
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
internal class BackofficeModule : ZeroModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Register(IZeroModuleConfiguration config)
|
||||
{
|
||||
config.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using System;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
public class ZeroBackofficeControllerModelConvention : IControllerModelConvention
|
||||
{
|
||||
readonly AttributeRouteModel RouteModel;
|
||||
|
||||
readonly Type BaseClass = typeof(ZeroBackofficeController);
|
||||
|
||||
|
||||
public ZeroBackofficeControllerModelConvention(string backofficePath)
|
||||
{
|
||||
RouteModel = new AttributeRouteModel(new RouteAttribute(backofficePath + "/api/[controller]/[action]"));
|
||||
}
|
||||
|
||||
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
if (!controller.ControllerType.IsSubclassOf(BaseClass))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var selector in controller.Selectors)
|
||||
{
|
||||
selector.AttributeRouteModel = RouteModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
class ZeroBackofficeMvcOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
IZeroOptions Options { get; set; }
|
||||
|
||||
public ZeroBackofficeMvcOptions(IZeroOptions options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
//options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.BackofficePath));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>zero.Backoffice</PackageId>
|
||||
<Version>0.1.0</Version>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace zero;
|
||||
|
||||
public interface IZeroRouteEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique hash for this entity (primarily used for routing)
|
||||
/// </summary>
|
||||
string Hash { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace zero;
|
||||
|
||||
[DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")]
|
||||
public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Full name of the entity
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias (non-unique) which can be used in the frontend and URLs
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A key which can be used to query this entity in code
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
public uint Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique hash for this entity (primarily used for routing)
|
||||
/// </summary>
|
||||
public string Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who last modified this content
|
||||
/// </summary>
|
||||
public string LastModifiedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of last modification
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModifiedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who created this content
|
||||
/// </summary>
|
||||
public string CreatedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of creation
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration of the base entity (which this one inherits from)
|
||||
/// </summary>
|
||||
public BlueprintConfiguration Blueprint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language of the entity
|
||||
/// </summary>
|
||||
public string LanguageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [Warning] This field is always empty when bound to the database.
|
||||
/// It is only filled in the app-code for routing.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace zero;
|
||||
|
||||
public class ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
/// <summary>
|
||||
/// An application is a website. zero can host multiple websites at once which share common assets
|
||||
/// </summary>
|
||||
[RavenCollection("Applications")]
|
||||
public class Application : ZeroEntity
|
||||
{
|
||||
/// <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();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace zero;
|
||||
|
||||
public class ApplicationOptions
|
||||
{
|
||||
public ApplicationOptions()
|
||||
{
|
||||
EnableMultiple = false;
|
||||
}
|
||||
|
||||
|
||||
public bool EnableMultiple { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace zero;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
/// <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 Id { 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();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using zero.Core.Blueprints;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class BlueprintOptions : ZeroBackofficeCollection<Blueprint>, IZeroCollectionOptions
|
||||
{
|
||||
public BlueprintOptions()
|
||||
{
|
||||
Enabled = false;
|
||||
}
|
||||
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
|
||||
public void Add<T>() where T : Blueprint, new()
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(Blueprint<T> implementation) where T : ZeroEntity, new()
|
||||
{
|
||||
Items.Add(implementation);
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(Action<Blueprint<T>> createExpression = null) where T : ZeroEntity, new()
|
||||
{
|
||||
Items.Add(new DefaultBlueprint<T>(createExpression));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero;
|
||||
|
||||
public class ZeroModule
|
||||
{
|
||||
public virtual void Register(IZeroModuleConfiguration config) { }
|
||||
|
||||
public virtual void Configure(IZeroOptions options) { }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroModuleConfiguration : IZeroModuleConfiguration
|
||||
{
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
internal ZeroModuleConfiguration(IServiceCollection servicse, IConfiguration configuration)
|
||||
{
|
||||
Services = servicse;
|
||||
Configuration = configuration;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroModuleConfiguration
|
||||
{
|
||||
IServiceCollection Services { get; }
|
||||
|
||||
IConfiguration Configuration { get; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class FeatureOptions : OptionsEnumerable<Feature>, IOptionsEnumerable
|
||||
{
|
||||
public void Add<T>() where T : Feature, new()
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
|
||||
public void Add(string alias, string name, string description)
|
||||
{
|
||||
Items.Add(new Feature()
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class IconOptions : OptionsEnumerable<IconSet>, IOptionsEnumerable
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a new backoffice icon set
|
||||
/// </summary>
|
||||
/// <param name="alias">Alias for reference</param>
|
||||
/// <param name="name">Name of the icon set</param>
|
||||
/// <param name="spritePath">Path to the SVG sprite containing addressable symbols</param>
|
||||
/// <param name="prefix">Optional symbol identifier prefix (by default the alias is used)</param>
|
||||
public void AddSet(string alias, string name, string spritePath, string prefix = null)
|
||||
{
|
||||
Items.Add(new IconSet()
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
SpritePath = spritePath,
|
||||
Prefix = prefix ?? alias
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using FluentValidation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Integrations;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class IntegrationOptions : OptionsEnumerable<IntegrationType>, IOptionsEnumerable
|
||||
{
|
||||
public void Add<T>(IntegrationType<T> integration) where T : Integration, new()
|
||||
{
|
||||
Items.Add(IntegrationType.Convert(integration));
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(string alias, string name, string description, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration, new()
|
||||
{
|
||||
Items.Add(new IntegrationType(typeof(T))
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
ImagePath = imagePath,
|
||||
Tags = tags,
|
||||
Validator = validator
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Add(Type type, string alias, string name, string description, List<string> tags = default, string imagePath = null, IValidator validator = null)
|
||||
{
|
||||
Items.Add(new IntegrationType(type)
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
ImagePath = imagePath,
|
||||
Tags = tags,
|
||||
Validator = validator
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using zero.Core.Collections;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class InterceptorOptions : OptionsEnumerable<InterceptorRegistration>, IOptionsEnumerable
|
||||
{
|
||||
public void Add<T>(int gravity = 0, Func<Type, bool> canHandle = null) where T : ICollectionInterceptor
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
if (canHandle == null)
|
||||
{
|
||||
canHandle = _ => true;
|
||||
}
|
||||
|
||||
Items.Add(new InterceptorRegistration()
|
||||
{
|
||||
Hash = IdGenerator.Create(),
|
||||
Name = type.Name,
|
||||
Gravity = gravity,
|
||||
CanHandle = canHandle,
|
||||
InterceptorType = type
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Add<T, TBoxed>(int gravity = 0, Func<Type, bool> canHandle = null)
|
||||
where T : ICollectionInterceptor<TBoxed>
|
||||
where TBoxed : ZeroEntity
|
||||
{
|
||||
Type type = typeof(T);
|
||||
Type boxedType = typeof(TBoxed);
|
||||
Func<Type, bool> finalCanHandle = requestedType =>
|
||||
{
|
||||
return boxedType.IsAssignableFrom(requestedType) && (canHandle == null || canHandle.Invoke(requestedType));
|
||||
};
|
||||
|
||||
Items.Add(new InterceptorRegistration()
|
||||
{
|
||||
Hash = IdGenerator.Create(),
|
||||
Name = type.Name,
|
||||
Gravity = gravity,
|
||||
CanHandle = finalCanHandle,
|
||||
InterceptorType = type,
|
||||
IsInterceptorBoxed = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class InterceptorRegistration
|
||||
{
|
||||
public int Gravity { get; set; }
|
||||
|
||||
public Type InterceptorType { get; set; }
|
||||
|
||||
public string Hash { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public Func<Type, bool> CanHandle { get; set; }
|
||||
|
||||
public bool IsInterceptorBoxed { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ModuleOptions : OptionsEnumerable<ModuleType>, IOptionsEnumerable
|
||||
{
|
||||
public void Add<T>(ModuleType<T> moduleType) where T : Module, new()
|
||||
{
|
||||
Items.Add(ModuleType.Convert(moduleType));
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(string alias, string name, string description, string icon, string group = null, List<string> tags = null, List<string> disallowedPageTypes = null) where T : Module, new()
|
||||
{
|
||||
Items.Add(new ModuleType(typeof(T))
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Group = group,
|
||||
Tags = tags ?? new List<string>(),
|
||||
DisallowedPageTypes = disallowedPageTypes ?? new List<string>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Add(Type type, string alias, string name, string description, string icon, string group = null, List<string> tags = null, List<string> disallowedPageTypes = null)
|
||||
{
|
||||
Items.Add(new ModuleType(type)
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Group = group,
|
||||
Tags = tags ?? new List<string>(),
|
||||
DisallowedPageTypes = disallowedPageTypes ?? new List<string>()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class PageOptions : OptionsEnumerable<PageType>, IOptionsEnumerable
|
||||
{
|
||||
public PageOptions()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public string Root { get; set; } = Constants.Pages.DefaultRootPageTypeAlias;
|
||||
|
||||
|
||||
public void Add<T>(PageType<T> pageType) where T : Page, new()
|
||||
{
|
||||
Items.Add(PageType.Convert(pageType));
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(string alias, string name, string description, string icon) where T : Page, new()
|
||||
{
|
||||
Items.Add(new PageType(typeof(T))
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Add(Type type, string alias, string name, string description, string icon)
|
||||
{
|
||||
Items.Add(new PageType(type)
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon
|
||||
});
|
||||
}
|
||||
|
||||
public PageType GetByAlias(string alias)
|
||||
{
|
||||
return Items.FirstOrDefault(x => x.Alias == alias);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Identity;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class PermissionOptions : OptionsEnumerable<PermissionCollection>, IOptionsEnumerable
|
||||
{
|
||||
public void AddCollection(PermissionCollection collection, int index = -1)
|
||||
{
|
||||
if (index > -1 && index < Items.Count)
|
||||
{
|
||||
Items.Insert(index, collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(collection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddCollection<T>(int index = -1) where T : PermissionCollection, new()
|
||||
{
|
||||
if (index > -1 && index < Items.Count)
|
||||
{
|
||||
Items.Insert(index, new T());
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Add(string collectionAlias, Permission permission, int index = -1)
|
||||
{
|
||||
PermissionCollection collection = Items.FirstOrDefault(x => x.Alias.Equals(collectionAlias, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (collection == null)
|
||||
{
|
||||
// TODO handle error
|
||||
return;
|
||||
}
|
||||
|
||||
IList<Permission> items = collection.Items;
|
||||
|
||||
if (index > -1 && index < items.Count)
|
||||
{
|
||||
items.Insert(index, permission);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Routing;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class RoutingEndpointOptions
|
||||
{
|
||||
HashSet<ResolverMap> Resolvers { get; set; } = new();
|
||||
|
||||
class ResolverMap
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
public Func<IRouteModel, RouteEndpoint> Impl { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(Func<T, RouteEndpoint> resolver) where T : class, IRouteModel
|
||||
{
|
||||
Resolvers.Add(new()
|
||||
{
|
||||
Type = typeof(T),
|
||||
Impl = obj => resolver(obj as T)
|
||||
});
|
||||
}
|
||||
|
||||
public void Replace<T>(Func<T, RouteEndpoint> resolver) where T : class, IRouteModel
|
||||
{
|
||||
Remove<T>();
|
||||
Add(resolver);
|
||||
}
|
||||
|
||||
public void Remove<T>() where T : IRouteModel
|
||||
{
|
||||
Type type = typeof(T);
|
||||
Resolvers.RemoveWhere(x => x.Type == type);
|
||||
}
|
||||
|
||||
public Func<IRouteModel, RouteEndpoint> Get<T>() where T : IRouteModel => Get(typeof(T));
|
||||
|
||||
public IEnumerable<Func<IRouteModel, RouteEndpoint>> GetAll<T>() where T : IRouteModel => GetAll(typeof(T));
|
||||
|
||||
public Func<IRouteModel, RouteEndpoint> Get(Type type)
|
||||
{
|
||||
ResolverMap map = Resolvers.LastOrDefault(x => x.Type == type);
|
||||
return map?.Impl;
|
||||
}
|
||||
|
||||
public IEnumerable<Func<IRouteModel, RouteEndpoint>> GetAll(Type type)
|
||||
{
|
||||
IEnumerable<ResolverMap> maps = Resolvers.Where(x => x.Type == type);
|
||||
return maps.Select(map => map.Impl).Where(x => x != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using zero.Core.Routing;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class RoutingOptions
|
||||
{
|
||||
public RoutingOptions()
|
||||
{
|
||||
PageRouteIdBuilder = new PageRouteIdBuilder();
|
||||
DefaultEndpoint = new("ZeroFrontend", "Index");
|
||||
EndpointResolvers = new();
|
||||
PageResolvers = new();
|
||||
//ErrorReexecutionPath = "/error";
|
||||
NotFoundEndpoint = null;
|
||||
}
|
||||
|
||||
|
||||
public IPageRouteIdBuilder PageRouteIdBuilder { get; set; }
|
||||
|
||||
|
||||
public RouteEndpoint NotFoundEndpoint { get; set; }
|
||||
|
||||
|
||||
public RouteEndpoint DefaultEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public RoutingEndpointOptions EndpointResolvers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public RoutingPageResolverOptions PageResolvers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string ErrorReexecutionPath { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class RoutingPageResolverOptions
|
||||
{
|
||||
HashSet<ResolverMap> Resolvers { get; set; } = new();
|
||||
|
||||
class ResolverMap
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
public Expression<Func<Page, bool>> Impl { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(Expression<Func<Page, bool>> resolver)
|
||||
{
|
||||
Resolvers.Add(new()
|
||||
{
|
||||
Type = typeof(T),
|
||||
Impl = resolver
|
||||
});
|
||||
}
|
||||
|
||||
public void Replace<T>(Expression<Func<Page, bool>> resolver)
|
||||
{
|
||||
Remove<T>();
|
||||
Add<T>(resolver);
|
||||
}
|
||||
|
||||
public void Remove<T>()
|
||||
{
|
||||
Type type = typeof(T);
|
||||
Resolvers.RemoveWhere(x => x.Type == type);
|
||||
}
|
||||
|
||||
public Expression<Func<Page, bool>> Get<T>() => Get(typeof(T));
|
||||
|
||||
public IEnumerable<Expression<Func<Page, bool>>> GetAll<T>() => GetAll(typeof(T));
|
||||
|
||||
public Expression<Func<Page, bool>> Get(Type type)
|
||||
{
|
||||
ResolverMap map = Resolvers.LastOrDefault(x => x.Type == type);
|
||||
return map?.Impl;
|
||||
}
|
||||
|
||||
public IEnumerable<Expression<Func<Page, bool>>> GetAll(Type type)
|
||||
{
|
||||
IEnumerable<ResolverMap> maps = Resolvers.Where(x => x.Type == type);
|
||||
return maps.Select(map => map.Impl).Where(x => x != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using Raven.Client.Documents;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Models;
|
||||
using zero.Core.Renderer;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class SearchOptions : OptionsEnumerable<SearchIndexMap>, IOptionsEnumerable
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
|
||||
public SearchOptions()
|
||||
{
|
||||
IsEnabled = true;
|
||||
//Map<Page>().Display((x, res, opts) =>
|
||||
//{
|
||||
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
|
||||
// if (pageType != null)
|
||||
// {
|
||||
// res.Icon = pageType.Icon;
|
||||
// }
|
||||
// res.Url = "/pages/edit/" + x.Id;
|
||||
//});
|
||||
//Map<MediaFolder>("fth-image");
|
||||
}
|
||||
|
||||
|
||||
public SearchIndexMap<T> Map<T>(string icon = null) where T : ZeroEntity, new()
|
||||
{
|
||||
SearchIndexMap<T> map = new(icon);
|
||||
Items.Add(map);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SearchIndexMap
|
||||
{
|
||||
internal Type Type;
|
||||
internal string _Icon;
|
||||
protected string _group;
|
||||
protected string[] _fields;
|
||||
protected Func<ZeroEntity, SearchResult, IZeroOptions, Task> _modify;
|
||||
|
||||
const string mapTemplate = @"map('{collection}', function (x) {
|
||||
return {
|
||||
Id: x.Id,
|
||||
Group: '{group}',
|
||||
Name: x.Name,
|
||||
IsActive: x.IsActive,
|
||||
Fields: [{fields}]
|
||||
};
|
||||
});";
|
||||
|
||||
internal SearchIndexMap(Type type, string icon = null)
|
||||
{
|
||||
Type = type;
|
||||
_group = "__TODO";
|
||||
_Icon = icon;
|
||||
}
|
||||
|
||||
internal string BuildInstruction(IDocumentStore store)
|
||||
{
|
||||
return TokenReplacement.Apply(mapTemplate, new()
|
||||
{
|
||||
{ "collection", store.Conventions.GetCollectionName(Type) },
|
||||
{ "group", _group },
|
||||
{ "fields", BuildFieldArray(_fields) }
|
||||
});
|
||||
}
|
||||
|
||||
internal string BuildFieldArray(string[] fields)
|
||||
{
|
||||
if (fields == null || !fields.Any())
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
return "x." + String.Join(", x.", fields);
|
||||
}
|
||||
|
||||
internal bool CanModify(Type type)
|
||||
{
|
||||
return Type.IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
internal async Task Modify(ZeroEntity entity, SearchResult result, IZeroOptions options)
|
||||
{
|
||||
if (_modify != null)
|
||||
{
|
||||
await _modify(entity, result, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SearchIndexMap<T> : SearchIndexMap where T : ZeroEntity
|
||||
{
|
||||
internal SearchIndexMap(string icon = null) : base(typeof(T), icon) {}
|
||||
|
||||
public SearchIndexMap<T> Icon(string icon)
|
||||
{
|
||||
_Icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchIndexMap<T> Display(Action<T, SearchResult> modify = null)
|
||||
{
|
||||
_modify = (x, res, opts) =>
|
||||
{
|
||||
modify?.Invoke(x as T, res);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchIndexMap<T> Display(Func<T, SearchResult, Task> modify = null)
|
||||
{
|
||||
_modify = (x, res, opts) => modify?.Invoke(x as T, res);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchIndexMap<T> Display(Action<T, SearchResult, IZeroOptions> modify = null)
|
||||
{
|
||||
_modify = (x, res, opts) =>
|
||||
{
|
||||
modify?.Invoke(x as T, res, opts);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchIndexMap<T> Display(Func<T, SearchResult, IZeroOptions, Task> modify = null)
|
||||
{
|
||||
_modify = (x, res, opts) => modify?.Invoke(x as T, res, opts);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchIndexMap<T> Fields(params string[] fieldNames)
|
||||
{
|
||||
_fields = fieldNames;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class SectionOptions : OptionsEnumerable<ISection>, IOptionsEnumerable
|
||||
{
|
||||
public SectionOptions()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>(int index = -1) where T : ISection, new()
|
||||
{
|
||||
if (index > -1 && index < Items.Count)
|
||||
{
|
||||
Items.Insert(index, new T());
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string alias, string name, string icon)
|
||||
{
|
||||
Items.Add(new Section(alias, name, icon));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace zero;
|
||||
|
||||
public class ServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Define a YouTube API Key so the zero videopicker can display previews
|
||||
/// </summary>
|
||||
public string YouTubeApiKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class SettingsOptions : OptionsEnumerable<SettingsGroup>, IOptionsEnumerable
|
||||
{
|
||||
public void AddGroup<T>(int index = -1) where T : SettingsGroup, new()
|
||||
{
|
||||
if (index > -1 && index < Items.Count)
|
||||
{
|
||||
Items.Insert(index, new T());
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddGroup(SettingsGroup group, int index = -1)
|
||||
{
|
||||
if (index > -1 && index < Items.Count)
|
||||
{
|
||||
Items.Insert(index, group);
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(group);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddToDefaultGroup(SettingsArea item, int index = -1)
|
||||
{
|
||||
SettingsGroup group = Items.FirstOrDefault(x => x.Name == "@settings.groups.system");
|
||||
|
||||
if (group == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > -1 && index < group.Items.Count)
|
||||
{
|
||||
group.Items.Insert(index, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
group.Items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class SpaceOptions : OptionsEnumerable<Space>, IOptionsEnumerable
|
||||
{
|
||||
public SpaceOptions()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void Add<T>() where T : Space, new()
|
||||
{
|
||||
Items.Add(new T());
|
||||
}
|
||||
|
||||
|
||||
public void AddList<T>(string alias, string name, string description, string icon) where T : SpaceContent, new()
|
||||
{
|
||||
Items.Add(new Space()
|
||||
{
|
||||
Alias = alias,
|
||||
View = SpaceView.List,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Type = typeof(T)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void AddEditor<T>(string alias, string name, string description, string icon) where T : SpaceContent, new()
|
||||
{
|
||||
Items.Add(new Space()
|
||||
{
|
||||
Alias = alias,
|
||||
View = SpaceView.Editor,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Type = typeof(T)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void AddSeparator()
|
||||
{
|
||||
Space lastSpace = Items.LastOrDefault();
|
||||
|
||||
if (lastSpace != null)
|
||||
{
|
||||
lastSpace.LineBelow = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddCustom<T>(string componentPath, string alias, string name, string description, string icon) where T : SpaceContent, new()
|
||||
{
|
||||
Items.Add(new Space()
|
||||
{
|
||||
Alias = alias,
|
||||
View = SpaceView.Custom,
|
||||
ComponentPath = componentPath,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Type = typeof(T)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void AddCustom(string componentPath, string alias, string name, string description, string icon)
|
||||
{
|
||||
Items.Add(new Space()
|
||||
{
|
||||
Alias = alias,
|
||||
View = SpaceView.Custom,
|
||||
ComponentPath = componentPath,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace zero;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const string ErrorFieldNone = "__zero_no_field";
|
||||
|
||||
public static class Tabs
|
||||
{
|
||||
public const string General = "general";
|
||||
}
|
||||
|
||||
public static class Auth
|
||||
{
|
||||
public const string SystemUser = "system";
|
||||
public const string DefaultScheme = "zeroScheme";
|
||||
public const string BackofficeDisplayName = "Zero Bacckoffice";
|
||||
public const string BackofficeScheme = "zeroBackoffice";
|
||||
public const string BackofficeCookieName = "zero.be.session";
|
||||
public const string DefaultCookieName = "zero.session";
|
||||
|
||||
public static class Claims
|
||||
{
|
||||
public const string IsZero = "zero.claim.iszero";
|
||||
public const string IsSuper = "zero.claim.issuper";
|
||||
public const string UserId = "zero.claim.userid";
|
||||
public const string UserName = "zero.claim.username";
|
||||
public const string Role = "zero.claim.rolealias";
|
||||
public const string SecurityStamp = "zero.claim.securitystamp";
|
||||
public const string Email = "zero.claim.email";
|
||||
public const string Permission = "zero.claim.permission";
|
||||
public const string DefaultAppId = "zero.claim.defaultAppId";
|
||||
public const string AppId = "zero.claim.appId";
|
||||
public const string TicketExpires = "zero.claim.ticketExpires";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Database
|
||||
{
|
||||
public const string ReservationPrefix = "zero.";
|
||||
public const string CoreIdPrefix = "core.";
|
||||
public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires;
|
||||
}
|
||||
|
||||
public static class Sections
|
||||
{
|
||||
public const string Dashboard = "dashboard";
|
||||
public const string Pages = "pages";
|
||||
public const string Spaces = "spaces";
|
||||
public const string Media = "media";
|
||||
public const string Settings = "settings";
|
||||
}
|
||||
|
||||
public static class Settings
|
||||
{
|
||||
public const string Updates = "updates";
|
||||
public const string Applications = "applications";
|
||||
public const string Users = "users";
|
||||
public const string Languages = "languages";
|
||||
public const string Translations = "translations";
|
||||
public const string Countries = "countries";
|
||||
public const string Mails = "mailTemplates";
|
||||
public const string Integrations = "integrations";
|
||||
public const string Logging = "logs";
|
||||
public const string Plugins = "plugins";
|
||||
public const string CreatePlugin = "createplugin";
|
||||
}
|
||||
|
||||
public static class PermissionCollections
|
||||
{
|
||||
public const string Sections = "permissionCollectionSections";
|
||||
public const string Spaces = "permissionCollectionSpaces";
|
||||
public const string Settings = "permisssionCollectionSettings";
|
||||
public const string Modules = "permissionCollectionModules";
|
||||
}
|
||||
|
||||
public static class Pages
|
||||
{
|
||||
public const string FolderAlias = "zero.folder";
|
||||
public const string DefaultRootPageTypeAlias = "root";
|
||||
public const string PageRouteProviderAlias = "zero.pages";
|
||||
}
|
||||
|
||||
public static class Routing
|
||||
{
|
||||
public const string InternalRoutePrefix = "/__zero/";
|
||||
|
||||
public const string ErrorRoute = InternalRoutePrefix + "error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero;
|
||||
|
||||
internal class ConfigurationModule : ZeroModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Register(IZeroModuleConfiguration config)
|
||||
{
|
||||
config.Services.AddOptions<ZeroOptions>()
|
||||
.Bind(config.Configuration.GetSection("Zero"))
|
||||
.Configure(opts => opts.ZeroVersion = "0.0.1.0" /*// TODO*/);
|
||||
|
||||
config.Services.AddTransient<IZeroOptions>(factory => factory.GetService<IOptionsMonitor<ZeroOptions>>().CurrentValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public abstract class OptionsEnumerable<T>
|
||||
{
|
||||
protected List<T> Items { get; set; } = new List<T>();
|
||||
|
||||
|
||||
public IReadOnlyCollection<T> GetAllItems()
|
||||
{
|
||||
return Items.AsReadOnly();
|
||||
}
|
||||
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
Items.RemoveAt(index);
|
||||
}
|
||||
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
return Items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IOptionsEnumerable { }
|
||||
@@ -0,0 +1,27 @@
|
||||
using FluentValidation;
|
||||
using System;
|
||||
|
||||
namespace zero;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroOptions : IZeroOptions
|
||||
{
|
||||
public ZeroOptions()
|
||||
{
|
||||
SupportedLanguages = new string[2] { "en-US", "de-DE" };
|
||||
DefaultLanguage = SupportedLanguages[0];
|
||||
TokenExpiration = 60 * 3;
|
||||
BackofficePath = "/zero";
|
||||
ExcludedPaths = new() { };
|
||||
Raven = new()
|
||||
{
|
||||
CollectionPrefix = String.Empty
|
||||
};
|
||||
Sections = new();
|
||||
Features = new();
|
||||
Pages = new();
|
||||
Modules = new();
|
||||
Permissions = new();
|
||||
Settings = new();
|
||||
Spaces = new();
|
||||
Integrations = new();
|
||||
Icons = new();
|
||||
Services = new();
|
||||
Blueprints = new();
|
||||
Routing = new();
|
||||
Interceptors = new();
|
||||
Applications = new();
|
||||
|
||||
//Raven.Indexes.Add<Backoffice_Search>();
|
||||
//Raven.Indexes.Add<Media_ByChildren>();
|
||||
//Raven.Indexes.Add<Media_ByParent>();
|
||||
//Raven.Indexes.Add<MediaFolder_ByHierarchy>();
|
||||
//Raven.Indexes.Add<MediaFolders_WithChildren>();
|
||||
//Raven.Indexes.Add<Pages_AsHistory>();
|
||||
//Raven.Indexes.Add<Pages_ByHierarchy>();
|
||||
//Raven.Indexes.Add<Pages_WithChildren>();
|
||||
//Raven.Indexes.Add<Pages_ByType>();
|
||||
//Raven.Indexes.Add<Routes_ForResolver>();
|
||||
//Raven.Indexes.Add<Routes_ByDependencies>();
|
||||
|
||||
//Raven.Indexes.Add<zero_Countries>();
|
||||
//Raven.Indexes.Add<zero_Languages>();
|
||||
//Raven.Indexes.Add<zero_Translations>();
|
||||
//Raven.Indexes.Add<zero_MailTemplates>();
|
||||
//Raven.Indexes.Add<zero_Spaces>();
|
||||
//Raven.Indexes.Add<zero_RecycledEntities>();
|
||||
|
||||
Search = new();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SetupCompleted => !String.IsNullOrEmpty(Raven?.Database);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ZeroVersion { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DefaultLanguage { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string[] SupportedLanguages { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TokenExpiration { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RavenOptions Raven { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApplicationOptions Applications { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string BackofficePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Paths in the backoffice which are not handled by zero
|
||||
/// </summary>
|
||||
public List<string> ExcludedPaths { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
//public IZeroPluginConfiguration Backoffice { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SectionOptions Sections { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public FeatureOptions Features { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PageOptions Pages { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ModuleOptions Modules { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PermissionOptions Permissions { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SettingsOptions Settings { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SpaceOptions Spaces { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntegrationOptions Integrations { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IconOptions Icons { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServiceOptions Services { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BlueprintOptions Blueprints { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RoutingOptions Routing { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public InterceptorOptions Interceptors { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchOptions Search { get; private set; }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the zero setup has already been completed and the instance is ready for use
|
||||
/// </summary>
|
||||
bool SetupCompleted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently active version
|
||||
/// This should not be set manually, as it is used for setup and migrations and incremented automatically
|
||||
/// </summary>
|
||||
string ZeroVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default language ISO code
|
||||
/// </summary>
|
||||
string DefaultLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language ISO codes which are supported by the zero backoffice
|
||||
/// </summary>
|
||||
string[] SupportedLanguages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expiration in minutes of a generated change token for an entity
|
||||
/// </summary>
|
||||
int TokenExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RavenDB configuration data
|
||||
/// </summary>
|
||||
RavenOptions Raven { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Application options
|
||||
/// </summary>
|
||||
ApplicationOptions Applications { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL path to the backoffice (defaults to /zero)
|
||||
/// </summary>
|
||||
string BackofficePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Paths in the backoffice which are not handled by zero
|
||||
/// </summary>
|
||||
List<string> ExcludedPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
SectionOptions Sections { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
FeatureOptions Features { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
PageOptions Pages { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
ModuleOptions Modules { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
PermissionOptions Permissions { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
SettingsOptions Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
SpaceOptions Spaces { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
IntegrationOptions Integrations { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
IconOptions Icons { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
ServiceOptions Services { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
BlueprintOptions Blueprints { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
RoutingOptions Routing { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
InterceptorOptions Interceptors { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
SearchOptions Search { get; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -3,186 +3,185 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
namespace zero;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return replaceMultipleSpacesRegex.Replace(value, SPACE).Trim();
|
||||
}
|
||||
|
||||
public static string TrimStart(this string value, string forRemoving)
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public static bool HasValue(this string input)
|
||||
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))
|
||||
{
|
||||
return !String.IsNullOrWhiteSpace(input);
|
||||
value = value.Substring(forRemoving.Length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public static bool IsNullOrEmpty(this string input)
|
||||
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 String.IsNullOrEmpty(input);
|
||||
return Char.ToLowerInvariant(input[0]) + input.Substring(1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
public static bool IsNullOrWhiteSpace(this string input)
|
||||
public static string ToPascalCase(this string input)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(input) && input.Length > 1)
|
||||
{
|
||||
return String.IsNullOrWhiteSpace(input);
|
||||
return Char.ToUpperInvariant(input[0]) + input.Substring(1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string ToCamelCase(this string input)
|
||||
public static string ToCamelCaseId(this string input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(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 (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))
|
||||
{
|
||||
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 (input.Length < 2)
|
||||
{
|
||||
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()));
|
||||
return input.ToUpperInvariant();
|
||||
}
|
||||
|
||||
string[] parts = input.Split('.');
|
||||
|
||||
public static string ToPascalCaseId(this string input)
|
||||
return String.Join(".", parts.Select(x => x.ToPascalCase()));
|
||||
}
|
||||
|
||||
|
||||
public static string RemoveNewLines(this string input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(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()));
|
||||
return input;
|
||||
}
|
||||
|
||||
return newLineCharsRegex.Replace(input, String.Empty).Trim();
|
||||
}
|
||||
|
||||
public static string RemoveNewLines(this string input)
|
||||
|
||||
public static string Shorten(this string input, int maxLength)
|
||||
{
|
||||
if (maxLength < 4)
|
||||
{
|
||||
if (String.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return newLineCharsRegex.Replace(input, String.Empty).Trim();
|
||||
throw new ArgumentOutOfRangeException("maxLength", "Has to be at least 4 characters long");
|
||||
}
|
||||
|
||||
|
||||
public static string Shorten(this string input, int maxLength)
|
||||
if (String.IsNullOrEmpty(input) || input.Length <= 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) + "...";
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers custom Raven conventions for database operations
|
||||
/// </summary>
|
||||
public interface IZeroDbConventions { }
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class IdGenerator
|
||||
{
|
||||
const string CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
private static Random random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new unique Id
|
||||
/// </summary>
|
||||
public static string Create(int length = -1)
|
||||
{
|
||||
if (length < 1)
|
||||
{
|
||||
length = 12;
|
||||
}
|
||||
|
||||
return new string(Enumerable.Repeat(CHARS, length).Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
|
||||
//if (length > 0)
|
||||
//{
|
||||
//return Convert.ToBase64String(Guid.NewGuid().ToByteArray())
|
||||
// .Replace("/", String.Empty)
|
||||
// .Replace("+", String.Empty)
|
||||
// .Replace("-", String.Empty)
|
||||
// .ToLowerInvariant()
|
||||
// .Substring(0, length);
|
||||
//}
|
||||
|
||||
//return Guid.NewGuid().ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Http;
|
||||
using System;
|
||||
|
||||
namespace zero;
|
||||
|
||||
internal class PersistenceModule : ZeroModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Register(IZeroModuleConfiguration config)
|
||||
{
|
||||
config.Services.AddSingleton<IZeroDocumentConventionsBuilder, ZeroDocumentConventionsBuilder>();
|
||||
config.Services.AddSingleton<IZeroDocumentStore, ZeroDocumentStore>(CreateRavenStore);
|
||||
config.Services.AddScoped<IZeroStore, ZeroStore>();
|
||||
config.Services.AddScoped<IZeroTokenProvider, ZeroTokenProvider>();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure(IZeroOptions options)
|
||||
{
|
||||
base.Configure(options);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures the raven store
|
||||
/// </summary>
|
||||
protected ZeroDocumentStore CreateRavenStore(IServiceProvider services)
|
||||
{
|
||||
IZeroOptions options = services.GetService<IZeroOptions>();
|
||||
IZeroDocumentConventionsBuilder conventionsBuilder = services.GetService<IZeroDocumentConventionsBuilder>();
|
||||
|
||||
IZeroDocumentStore store = new ZeroDocumentStore(options)
|
||||
{
|
||||
Urls = new string[1] { options.Raven.Url },
|
||||
Conventions = // TODO activate and test this
|
||||
{
|
||||
AggressiveCache =
|
||||
{
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
Mode = AggressiveCacheMode.TrackChanges
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
conventionsBuilder.Run(store.Conventions);
|
||||
|
||||
IDocumentStore raven = store.Initialize();
|
||||
|
||||
// create all indexes
|
||||
if (options.SetupCompleted)
|
||||
{
|
||||
//var indexes = options.Raven.Indexes.BuildAll(options, store);
|
||||
//IndexCreation.CreateIndexes(indexes, store, database: options.Raven.Database);
|
||||
}
|
||||
|
||||
return (ZeroDocumentStore)raven;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace zero;
|
||||
|
||||
/// <summary>
|
||||
/// This attribute will allow the usage of custom collection names for Raven collections
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class RavenCollectionAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public RavenCollectionAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Raven.Client.Documents;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class RavenOptions
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public string Database { get; set; }
|
||||
|
||||
public string CollectionPrefix { get; set; }
|
||||
|
||||
public RavenIndexesOptions Indexes { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public class RavenIndexesOptions : OptionsEnumerable<RavenIndexesOptions.Map>, IOptionsEnumerable
|
||||
{
|
||||
public class Map
|
||||
{
|
||||
internal Type Type { get; set; }
|
||||
|
||||
internal Expression<Func<IZeroIndexDefinition>> CreateIndex { get; set; }
|
||||
|
||||
internal Map(Type type, Expression<Func<IZeroIndexDefinition>> create)
|
||||
{
|
||||
Type = type;
|
||||
CreateIndex = create;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public RavenIndexModifiersOptions Modifiers { get; private set; } = new();
|
||||
|
||||
public void Add<T>() where T : IZeroIndexDefinition, new()
|
||||
{
|
||||
Items.Add(new(typeof(T), () => new T()));
|
||||
}
|
||||
|
||||
public void Add(Type indexType)
|
||||
{
|
||||
Items.Add(new(indexType, () => (IZeroIndexDefinition)Activator.CreateInstance(indexType)));
|
||||
}
|
||||
|
||||
public void Add<T>(T index) where T : IZeroIndexDefinition
|
||||
{
|
||||
Items.Add(new(typeof(T), () => index));
|
||||
}
|
||||
|
||||
public void AddRange(params Type[] indexes)
|
||||
{
|
||||
foreach (Type type in indexes)
|
||||
{
|
||||
Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void Replace<T, TReplaceWith>()
|
||||
where T : IZeroIndexDefinition, new()
|
||||
where TReplaceWith : IZeroIndexDefinition, new()
|
||||
{
|
||||
Replace(typeof(T), typeof(TReplaceWith));
|
||||
}
|
||||
|
||||
public void Replace(Type origin, Type replaceWith)
|
||||
{
|
||||
var item = Items.FirstOrDefault(x => x.Type == origin);
|
||||
if (item != null)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
Add(replaceWith);
|
||||
}
|
||||
|
||||
public IEnumerable<IZeroIndexDefinition> BuildAll(IZeroOptions options, IDocumentStore store)
|
||||
{
|
||||
foreach (Map map in Items)
|
||||
{
|
||||
IZeroIndexDefinition index = map.CreateIndex.Compile().Invoke();
|
||||
index.Setup(options, store);
|
||||
index.RunModifiers(options);
|
||||
yield return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class RavenIndexModifiersOptions : OptionsEnumerable<RavenIndexModifiersOptions.Modifier>, IOptionsEnumerable
|
||||
{
|
||||
public class Modifier
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
|
||||
public Expression<Action<IZeroIndexDefinition>> Modify { get; set; }
|
||||
}
|
||||
|
||||
public void Add<T>(Action<T> modify) where T : IZeroIndexDefinition, new()
|
||||
{
|
||||
Items.Add(new()
|
||||
{
|
||||
Type = typeof(T),
|
||||
Modify = x => modify((T)x)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<Modifier> GetAllForType<T>() where T : IZeroIndexDefinition, new() => GetAllForType(typeof(T));
|
||||
|
||||
|
||||
public IEnumerable<Modifier> GetAllForType(Type type)
|
||||
{
|
||||
foreach (Modifier modifier in Items.Where(x => x.Type.IsAssignableFrom(type)))
|
||||
{
|
||||
yield return modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//using Raven.Client.Documents;
|
||||
//using Raven.Client.Documents.Linq;
|
||||
//using Raven.Client.Documents.Session;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Linq;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
//namespace zero;
|
||||
|
||||
//public static class RavenQueryableExtensions
|
||||
//{
|
||||
// // TODO we need to simplify these extensions methods.
|
||||
// // ToQueriedListAsyncX is used in MediaCollection for the Media_ByParent index, which produces MediaListItem (which is no ZeroEntity)
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// public static async Task<ListResult<T>> ToQueriedListAsyncX<T>(this IRavenQueryable<T> queryable, ListQuery<T> query)
|
||||
// {
|
||||
// queryable = queryable.Statistics(out QueryStatistics stats);
|
||||
|
||||
// IQueryable<T> rawQuery = queryable;
|
||||
|
||||
// if (query != null)
|
||||
// {
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
// }
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
|
||||
// {
|
||||
// foreach (var selector in query.SearchSelectors)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (query.OrderQuery != null)
|
||||
// {
|
||||
// rawQuery = query.OrderQuery(rawQuery);
|
||||
// }
|
||||
// else if (!query.OrderBy.IsNullOrEmpty())
|
||||
// {
|
||||
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
|
||||
// }
|
||||
|
||||
// if (query.PageSize > 0)
|
||||
// {
|
||||
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
|
||||
// }
|
||||
// }
|
||||
|
||||
// List<T> items = await rawQuery.ToListAsync();
|
||||
|
||||
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
|
||||
// }
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// public static async Task<ListResult<T>> ToQueriedListAsync<T>(this IRavenQueryable<T> queryable, ListQuery<T> query) where T : ZeroEntity
|
||||
// {
|
||||
// queryable = queryable.Statistics(out QueryStatistics stats);
|
||||
|
||||
// IQueryable<T> rawQuery = queryable;
|
||||
|
||||
// if (query != null)
|
||||
// {
|
||||
// if (!query.IncludeInactive)
|
||||
// {
|
||||
// rawQuery = rawQuery.Where(x => x.IsActive);
|
||||
// }
|
||||
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
|
||||
// {
|
||||
// foreach (var selector in query.SearchSelectors)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
// }
|
||||
// }
|
||||
// else if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
// }
|
||||
|
||||
// if (query.OrderQuery != null)
|
||||
// {
|
||||
// rawQuery = query.OrderQuery(rawQuery);
|
||||
// }
|
||||
// else if (!query.OrderBy.IsNullOrEmpty())
|
||||
// {
|
||||
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// rawQuery = rawQuery.OrderByDescending(x => x.CreatedDate);
|
||||
// }
|
||||
|
||||
// if (query.PageSize > 0)
|
||||
// {
|
||||
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
|
||||
// }
|
||||
// }
|
||||
|
||||
// List<T> items = await rawQuery.ToListAsync();
|
||||
|
||||
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
|
||||
// }
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// public static async Task<ListResult<T>> ToQueriedListAsyncX<T, TFilter>(this IRavenQueryable<T> queryable, ListQuery<T, TFilter> query) where TFilter : IListSpecificQuery
|
||||
// {
|
||||
// queryable = queryable.Statistics(out QueryStatistics stats);
|
||||
|
||||
// IQueryable<T> rawQuery = queryable;
|
||||
|
||||
// if (query != null)
|
||||
// {
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*");
|
||||
// }
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
|
||||
// {
|
||||
// foreach (var selector in query.SearchSelectors)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!query.OrderBy.IsNullOrEmpty())
|
||||
// {
|
||||
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
|
||||
// }
|
||||
|
||||
// if (query.PageSize > 0)
|
||||
// {
|
||||
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
|
||||
// }
|
||||
// }
|
||||
|
||||
// List<T> items = await rawQuery.ToListAsync();
|
||||
|
||||
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
|
||||
// }
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// public static async Task<ListResult<T>> ToQueriedListAsync<T, TFilter>(this IRavenQueryable<T> queryable, ListQuery<T, TFilter> query) where T : ZeroEntity where TFilter : IListSpecificQuery
|
||||
// {
|
||||
// queryable = queryable.Statistics(out QueryStatistics stats);
|
||||
|
||||
// IQueryable<T> rawQuery = queryable;
|
||||
|
||||
// if (query != null)
|
||||
// {
|
||||
// if (!query.IncludeInactive)
|
||||
// {
|
||||
// rawQuery = rawQuery.Where(x => x.IsActive);
|
||||
// }
|
||||
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*");
|
||||
// }
|
||||
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
|
||||
// {
|
||||
// foreach (var selector in query.SearchSelectors)
|
||||
// {
|
||||
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!query.OrderBy.IsNullOrEmpty())
|
||||
// {
|
||||
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
|
||||
// }
|
||||
|
||||
// if (query.PageSize > 0)
|
||||
// {
|
||||
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
|
||||
// }
|
||||
// }
|
||||
|
||||
// List<T> items = await rawQuery.ToListAsync();
|
||||
|
||||
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace zero;
|
||||
|
||||
/// <summary>
|
||||
/// Replicates the Rfc6238AuthenticationService from ASP.NET Core internals as it isn't part of the public API
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs)
|
||||
/// </summary>
|
||||
public static class Rfc6238AuthenticationService
|
||||
{
|
||||
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
|
||||
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
|
||||
|
||||
// Generates a new 80-bit security token
|
||||
public static byte[] GenerateRandomKey()
|
||||
{
|
||||
byte[] bytes = new byte[20];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
|
||||
{
|
||||
// # of 0's = length of pin
|
||||
const int Mod = 1000000;
|
||||
|
||||
// See https://tools.ietf.org/html/rfc4226
|
||||
// We can add an optional modifier
|
||||
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
|
||||
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));
|
||||
|
||||
// Generate DT string
|
||||
var offset = hash[hash.Length - 1] & 0xf;
|
||||
Debug.Assert(offset + 4 < hash.Length);
|
||||
var binaryCode = (hash[offset] & 0x7f) << 24
|
||||
| (hash[offset + 1] & 0xff) << 16
|
||||
| (hash[offset + 2] & 0xff) << 8
|
||||
| (hash[offset + 3] & 0xff);
|
||||
|
||||
return binaryCode % Mod;
|
||||
}
|
||||
|
||||
private static byte[] ApplyModifier(byte[] input, string modifier)
|
||||
{
|
||||
if (String.IsNullOrEmpty(modifier))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var modifierBytes = _encoding.GetBytes(modifier);
|
||||
var combined = new byte[checked(input.Length + modifierBytes.Length)];
|
||||
Buffer.BlockCopy(input, 0, combined, 0, input.Length);
|
||||
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// More info: https://tools.ietf.org/html/rfc6238#section-4
|
||||
private static ulong GetCurrentTimeStepNumber()
|
||||
{
|
||||
var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;
|
||||
return (ulong)(delta.Ticks / _timestep.Ticks);
|
||||
}
|
||||
|
||||
public static int GenerateCode(byte[] securityToken, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using (var hashAlgorithm = new HMACSHA1(securityToken))
|
||||
{
|
||||
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ValidateCode(byte[] securityToken, int code, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using (var hashAlgorithm = new HMACSHA1(securityToken))
|
||||
{
|
||||
for (var i = -2; i <= 2; i++)
|
||||
{
|
||||
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
|
||||
if (computedTotp == code)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
[RavenCollection("Tokens")]
|
||||
public class SecurityToken : IZeroDbConventions
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroTokenProvider : IZeroTokenProvider
|
||||
{
|
||||
readonly char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-".ToCharArray();
|
||||
|
||||
readonly RandomNumberGenerator randonNumberGenerator;
|
||||
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
|
||||
public ZeroTokenProvider(IZeroStore store)
|
||||
{
|
||||
Store = store;
|
||||
randonNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default)
|
||||
{
|
||||
if (key.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new ArgumentException("Key cannot be empty");
|
||||
}
|
||||
if (length < 16)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Use at least a length of 16 for the token");
|
||||
}
|
||||
|
||||
string tokenKey = Random(length);
|
||||
|
||||
SecurityToken securityToken = new SecurityToken()
|
||||
{
|
||||
Id = TokenToId(tokenKey),
|
||||
Token = tokenKey,
|
||||
Key = HashKey(key),
|
||||
Metadata = metadata ?? new()
|
||||
};
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// saves the token
|
||||
await session.StoreAsync(securityToken);
|
||||
|
||||
// set the expires flag for the token
|
||||
IMetadataDictionary tokenMetadata = session.Advanced.GetMetadataFor(securityToken);
|
||||
tokenMetadata[Constants.Database.Expires] = DateTime.UtcNow.AddSeconds(expires.TotalSeconds);
|
||||
await session.SaveChangesAsync();
|
||||
|
||||
return tokenKey;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> Verify(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<SecurityToken> VerifyAndReturn(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
return securityToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Random(int length)
|
||||
{
|
||||
byte[] data = new byte[4 * length];
|
||||
randonNumberGenerator.GetBytes(data);
|
||||
|
||||
StringBuilder result = new(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var rnd = BitConverter.ToUInt32(data, i * 4);
|
||||
var idx = rnd % chars.Length;
|
||||
|
||||
result.Append(chars[idx]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts the token to a database id
|
||||
/// </summary>
|
||||
string TokenToId(string token)
|
||||
{
|
||||
string collection = Store.Raven.Conventions.GetCollectionName(typeof(SecurityToken));
|
||||
string idPrefix = Store.Raven.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(collection);
|
||||
return idPrefix + Store.Raven.Conventions.IdentityPartsSeparator + token;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hash for the security token key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
string HashKey(string key)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256;
|
||||
int iterationCount = 10000;
|
||||
int saltSize = 128 / 8;
|
||||
int numBytesRequested = 256 / 8;
|
||||
|
||||
byte[] salt = new byte[saltSize];
|
||||
randonNumberGenerator.GetBytes(salt);
|
||||
byte[] subkey = KeyDerivation.Pbkdf2(key, salt, prf, iterationCount, numBytesRequested);
|
||||
|
||||
var outputBytes = new byte[13 + salt.Length + subkey.Length];
|
||||
outputBytes[0] = 0x01;
|
||||
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
|
||||
WriteNetworkByteOrder(outputBytes, 5, (uint)iterationCount);
|
||||
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
|
||||
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
|
||||
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
|
||||
return Convert.ToBase64String(outputBytes);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the given key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
bool VerifyKey(string storedKey, string key)
|
||||
{
|
||||
byte[] decodedStoredKey = Convert.FromBase64String(storedKey);
|
||||
|
||||
try
|
||||
{
|
||||
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedStoredKey, 1);
|
||||
int embeddedIterCount = (int)ReadNetworkByteOrder(decodedStoredKey, 5);
|
||||
int saltLength = (int)ReadNetworkByteOrder(decodedStoredKey, 9);
|
||||
|
||||
// Read the salt: must be >= 128 bits
|
||||
if (saltLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] salt = new byte[saltLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13, salt, 0, salt.Length);
|
||||
|
||||
// Read the subkey (the rest of the payload): must be >= 128 bits
|
||||
int subkeyLength = decodedStoredKey.Length - 13 - salt.Length;
|
||||
if (subkeyLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
byte[] expectedSubkey = new byte[subkeyLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
|
||||
|
||||
// Hash the incoming key and verify it
|
||||
byte[] actualSubkey = KeyDerivation.Pbkdf2(key, salt, prf, embeddedIterCount, subkeyLength);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
|
||||
{
|
||||
buffer[offset + 0] = (byte)(value >> 24);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 3] = (byte)(value >> 0);
|
||||
}
|
||||
|
||||
static uint ReadNetworkByteOrder(byte[] buffer, int offset)
|
||||
{
|
||||
return ((uint)(buffer[offset + 0]) << 24)
|
||||
| ((uint)(buffer[offset + 1]) << 16)
|
||||
| ((uint)(buffer[offset + 2]) << 8)
|
||||
| ((uint)(buffer[offset + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroTokenProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a token for a <paramref name="key"/> with a specified <paramref name="expires"/> lifespan.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token will be used for. The key must match when verifying the token and should always be hidden from the user.</param>
|
||||
/// <param name="expires">The token will automatically expire after this timespan.</param>
|
||||
/// <param name="length">Length of the geneated token.</param>
|
||||
/// <param name="metadata">Additional metadata to store with the token. This data can be retrieved on verification with <see cref="VerifyAndReturn(string, string)"/></param>
|
||||
/// <returns>
|
||||
/// The generated token with the specified <paramref name="length"/>.
|
||||
/// </returns>
|
||||
Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// False, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<bool> Verify(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// Null, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<SecurityToken> VerifyAndReturn(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random token with the specified <paramref name="length"/> using the RNGCryptoServiceProvider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method won't store the token in the database and can't be used for verification. Use <see cref="Create(string, TimeSpan, int)"/> instead.
|
||||
/// </remarks>
|
||||
string Random(int length);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Raven.Client.Documents.Conventions;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroDocumentConventionsBuilder : IZeroDocumentConventionsBuilder
|
||||
{
|
||||
protected HashSet<Type> PolymorphTypes { get; private set; } = new();
|
||||
|
||||
protected Type AcceptsZeroConventionsType { get; set; } = typeof(IZeroDbConventions);
|
||||
|
||||
protected char IdentityPartsSeparator { get; set; } = '.';
|
||||
|
||||
protected IZeroOptions Options { get; private set; }
|
||||
|
||||
protected static ConcurrentDictionary<Type, string> CachedTypeCollectionNameMap = new();
|
||||
|
||||
|
||||
public ZeroDocumentConventionsBuilder(IZeroOptions options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(DocumentConventions conventions)
|
||||
{
|
||||
conventions.MaxNumberOfRequestsPerSession = 1000;
|
||||
conventions.IdentityPartsSeparator = IdentityPartsSeparator;
|
||||
conventions.TransformTypeCollectionNameToDocumentIdPrefix = TransformTypeCollectionNameToDocumentIdPrefix;
|
||||
conventions.FindCollectionName = FindCollectionName;
|
||||
conventions.RegisterAsyncIdConvention<ZeroIdEntity>((_, entity) => GetDocumentId(conventions, entity));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get a document ID from an entity
|
||||
/// </summary>
|
||||
protected virtual Task<string> GetDocumentId<T>(DocumentConventions conventions, T entity)
|
||||
{
|
||||
string collection = conventions.GetCollectionName(entity);
|
||||
|
||||
StringBuilder documentId = new();
|
||||
documentId.Append(conventions.TransformTypeCollectionNameToDocumentIdPrefix(collection));
|
||||
documentId.Append(IdentityPartsSeparator);
|
||||
documentId.Append(IdGenerator.Create());
|
||||
|
||||
return Task.FromResult(documentId.ToString());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Finds the collection name for a certain type based on internal rules
|
||||
/// </summary>
|
||||
protected virtual string FindCollectionName(Type originalType)
|
||||
{
|
||||
string collection = null;
|
||||
|
||||
Type type = originalType;
|
||||
|
||||
Func<string, string> cache = name =>
|
||||
{
|
||||
CachedTypeCollectionNameMap.TryAdd(type, name);
|
||||
return name;
|
||||
};
|
||||
|
||||
// get inner type for revisions
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Raven.Client.Documents.Subscriptions.Revision<>))
|
||||
{
|
||||
type = type.GetGenericArguments().FirstOrDefault();
|
||||
}
|
||||
|
||||
// try to resolve from cache
|
||||
if (CachedTypeCollectionNameMap.TryGetValue(type, out collection))
|
||||
{
|
||||
return collection;
|
||||
}
|
||||
|
||||
// do not alter non-internal entities
|
||||
if (!AcceptsZeroConventionsType.IsAssignableFrom(type))
|
||||
{
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(type));
|
||||
}
|
||||
|
||||
// use name from attribute if available
|
||||
RavenCollectionAttribute collectionAttribute = type.GetCustomAttribute<RavenCollectionAttribute>(true);
|
||||
|
||||
if (collectionAttribute != null)
|
||||
{
|
||||
return cache(collectionAttribute.Name);
|
||||
}
|
||||
|
||||
// use base interface if available
|
||||
Type interfaceBaseType = type.GetInterfaces().FirstOrDefault(x => x.IsInterface && AcceptsZeroConventionsType.IsAssignableFrom(x) && x.Name != AcceptsZeroConventionsType.Name);
|
||||
|
||||
if (interfaceBaseType != null)
|
||||
{
|
||||
// use name from attribute if available
|
||||
collectionAttribute = interfaceBaseType.GetCustomAttribute<RavenCollectionAttribute>(true);
|
||||
if (collectionAttribute != null)
|
||||
{
|
||||
return cache(collectionAttribute.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// use base type for polymorphism
|
||||
Type polymorphBaseType = PolymorphTypes.FirstOrDefault(x => type.IsSubclassOf(x));
|
||||
|
||||
if (polymorphBaseType != null)
|
||||
{
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(polymorphBaseType));
|
||||
}
|
||||
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(type));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Translates the types collection name to the document id prefix
|
||||
/// </summary>
|
||||
protected virtual string TransformTypeCollectionNameToDocumentIdPrefix(string name)
|
||||
{
|
||||
if (!Options.Raven.CollectionPrefix.IsNullOrWhiteSpace())
|
||||
{
|
||||
name = Options.Raven.CollectionPrefix.EnsureEndsWith(IdentityPartsSeparator) + name.TrimStart(Options.Raven.CollectionPrefix);
|
||||
}
|
||||
|
||||
return name.ToCamelCaseId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentConventionsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies internal rules to the RavenDB document conventions
|
||||
/// </summary>
|
||||
void Run(DocumentConventions conventions);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession
|
||||
{
|
||||
public IZeroDocumentSession Core { get; private set; }
|
||||
|
||||
public ZeroDocumentSession(DocumentStore documentStore, Guid id, SessionOptions options, string coreDatabase = null) : base(documentStore, id, options)
|
||||
{
|
||||
if (coreDatabase.HasValue())
|
||||
{
|
||||
Core = new ZeroDocumentSession(documentStore, id, new SessionOptions()
|
||||
{
|
||||
Database = coreDatabase,
|
||||
DisableAtomicDocumentWritesInClusterWideTransaction = options.DisableAtomicDocumentWritesInClusterWideTransaction,
|
||||
NoCaching = options.NoCaching,
|
||||
NoTracking = options.NoTracking,
|
||||
RequestExecutor = options.RequestExecutor,
|
||||
TransactionMode = options.TransactionMode
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Core = this;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentSession : IAsyncDocumentSession
|
||||
{
|
||||
IZeroDocumentSession Core { get; }
|
||||
|
||||
bool IsDisposed { get; }
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using Raven.Client;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.BulkInsert;
|
||||
using Raven.Client.Documents.Operations;
|
||||
using Raven.Client.Documents.Queries;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore
|
||||
{
|
||||
public ZeroDocumentStore(IZeroOptions options) : base()
|
||||
{
|
||||
Options = options;
|
||||
Database = null;
|
||||
}
|
||||
|
||||
protected IZeroOptions Options { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ResolvedDatabase { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public OperationExecutor GetOperationExecutor(string database = null)
|
||||
{
|
||||
return Operations.ForDatabase(database ?? ResolvedDatabase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession(string database)
|
||||
{
|
||||
return OpenAsyncSession(new SessionOptions()
|
||||
{
|
||||
Database = database
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession(SessionOptions options)
|
||||
{
|
||||
options.Database = options.Database ?? ResolvedDatabase;
|
||||
|
||||
AssertInitialized();
|
||||
EnsureNotClosed();
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var session = new ZeroDocumentSession(this, sessionId, options, Options.Raven.Database);
|
||||
RegisterEvents(session);
|
||||
AfterSessionCreated(session);
|
||||
session.OnSessionDisposing += (sender, args) =>
|
||||
{
|
||||
session.IsDisposed = true;
|
||||
};
|
||||
|
||||
session.Advanced.WaitForIndexesAfterSaveChanges();
|
||||
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession()
|
||||
{
|
||||
return OpenAsyncSession(new SessionOptions()
|
||||
{
|
||||
Database = ResolvedDatabase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession(SessionOptions options)
|
||||
{
|
||||
options.Database = options.Database ?? ResolvedDatabase;
|
||||
|
||||
AssertInitialized();
|
||||
EnsureNotClosed();
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var session = new DocumentSession(this, sessionId, options);
|
||||
RegisterEvents(session);
|
||||
AfterSessionCreated(session);
|
||||
|
||||
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession(string database)
|
||||
{
|
||||
return OpenSession(new SessionOptions()
|
||||
{
|
||||
Database = database
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession()
|
||||
{
|
||||
return OpenSession(new SessionOptions()
|
||||
{
|
||||
Database = ResolvedDatabase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BulkInsertOperation BulkInsert(string database = null, CancellationToken token = default)
|
||||
{
|
||||
return base.BulkInsert(database ?? ResolvedDatabase, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PurgeAsync<T>(string database = null, string querySuffix = null, Parameters parameters = null)
|
||||
{
|
||||
var collectionName = Conventions.FindCollectionName(typeof(T));
|
||||
var operationQuery = new DeleteByQueryOperation(new IndexQuery()
|
||||
{
|
||||
Query = $"from {collectionName} c {querySuffix ?? String.Empty}",
|
||||
QueryParameters = parameters
|
||||
}, new QueryOperationOptions { AllowStale = true });
|
||||
|
||||
Operation operation = await GetOperationExecutor(database ?? ResolvedDatabase).SendAsync(operationQuery);
|
||||
|
||||
await operation.WaitForCompletionAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentStore : IDocumentStore
|
||||
{
|
||||
/// <summary>
|
||||
/// The database which has been resolved from the current application
|
||||
/// </summary>
|
||||
string ResolvedDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get operation executor
|
||||
/// </summary>
|
||||
OperationExecutor GetOperationExecutor(string database = null);
|
||||
|
||||
/// <summary>
|
||||
/// Purges a collection
|
||||
/// </summary>
|
||||
Task PurgeAsync<T>(string database = null, string querySuffix = null, Parameters parameters = null);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using Raven.Client.Documents.Indexes.Spatial;
|
||||
using Raven.Client.Documents.Operations.Attachments;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public abstract class ZeroJavascriptIndex : AbstractJavaScriptIndexCreationTask, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroJavascriptIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
public new string Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new string PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
public abstract class ZeroMultiMapIndex : AbstractMultiMapIndexCreationTask<object>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroMultiMapIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractMultiMapIndexCreationTask<TReduceResult>
|
||||
public new void AddMap<TSource>(Expression<Func<IEnumerable<TSource>, IEnumerable>> map) => base.AddMap(map);
|
||||
public new void AddMapForAll<TBase>(Expression<Func<IEnumerable<TBase>, IEnumerable>> map) => base.AddMapForAll(map);
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<object>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<object, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<object, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<object, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<object, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<object, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<object, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<object, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<object, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroMultiMapIndex<TReduceResult> : AbstractMultiMapIndexCreationTask<TReduceResult>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroMultiMapIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractMultiMapIndexCreationTask<TReduceResult>
|
||||
public new void AddMap<TSource>(Expression<Func<IEnumerable<TSource>, IEnumerable>> map) => base.AddMap(map);
|
||||
public new void AddMapForAll<TBase>(Expression<Func<IEnumerable<TBase>, IEnumerable>> map) => base.AddMapForAll(map);
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TReduceResult>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<TReduceResult, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<TReduceResult, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<TReduceResult, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<TReduceResult, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<TReduceResult, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<TReduceResult, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<TReduceResult, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<TReduceResult, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex<TDocument, TReduceResult> : AbstractIndexCreationTask<TDocument, TReduceResult>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractIndexCreationTask<TDocument, TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TDocument>, IEnumerable>> Map { get => base.Map; set => base.Map = value; }
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TReduceResult>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<TReduceResult, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<TReduceResult, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<TReduceResult, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<TReduceResult, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<TReduceResult, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<TReduceResult, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<TReduceResult, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<TReduceResult, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex<TDocument> : ZeroIndex<TDocument, TDocument>
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex : AbstractIndexCreationTask, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroIndexDefinition : IAbstractIndexCreationTask
|
||||
{
|
||||
void Setup(IZeroOptions options, IDocumentStore store);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public static class ZeroIndexExtensions
|
||||
{
|
||||
internal static void RunModifiers<T>(this T index, IZeroOptions options) where T : IZeroIndexDefinition
|
||||
{
|
||||
IEnumerable<RavenIndexModifiersOptions.Modifier> modifiers = options.Raven.Indexes.Modifiers.GetAllForType(index.GetType());
|
||||
|
||||
foreach (RavenIndexModifiersOptions.Modifier modifier in modifiers)
|
||||
{
|
||||
Action<IZeroIndexDefinition> action = modifier.Modify.Compile();
|
||||
action.Invoke(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public class ZeroStore : IZeroStore
|
||||
{
|
||||
public ZeroStore(IZeroDocumentStore raven, IZeroOptions options) : base()
|
||||
{
|
||||
Options = options;
|
||||
Raven = raven;
|
||||
//Database = null;
|
||||
}
|
||||
|
||||
protected IZeroOptions Options { get; set; }
|
||||
protected Dictionary<string, IZeroDocumentSession> ScopedSessions { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ResolvedDatabase { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentStore Raven { get; private set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentSession Session(string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null)
|
||||
{
|
||||
database ??= ResolvedDatabase;
|
||||
options ??= new SessionOptions() { Database = database };
|
||||
|
||||
if (resolution == ZeroSessionResolution.Create)
|
||||
{
|
||||
return Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
}
|
||||
|
||||
if (!ScopedSessions.TryGetValue(database, out IZeroDocumentSession session))
|
||||
{
|
||||
session = Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
ScopedSessions.TryAdd(database, session);
|
||||
}
|
||||
|
||||
if (session.IsDisposed)
|
||||
{
|
||||
session = Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
ScopedSessions.Remove(database);
|
||||
ScopedSessions.TryAdd(database, session);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentSession Session(bool global, string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null)
|
||||
{
|
||||
return Session(global ? Options.Raven.Database : database, resolution, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum ZeroSessionResolution
|
||||
{
|
||||
Reuse = 0,
|
||||
Create = 1
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroStore
|
||||
{
|
||||
/// <summary>
|
||||
/// The database which has been resolved from the current application
|
||||
/// </summary>
|
||||
string ResolvedDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get underlying raven document store
|
||||
/// </summary>
|
||||
IZeroDocumentStore Raven { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Use a specific session
|
||||
/// </summary>
|
||||
IZeroDocumentSession Session(string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Use a session for the core database
|
||||
/// </summary>
|
||||
IZeroDocumentSession Session(bool global, string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Core.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Replicates the Rfc6238AuthenticationService from ASP.NET Core internals as it isn't part of the public API
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs)
|
||||
/// </summary>
|
||||
public static class Rfc6238AuthenticationService
|
||||
{
|
||||
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
|
||||
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
|
||||
|
||||
// Generates a new 80-bit security token
|
||||
public static byte[] GenerateRandomKey()
|
||||
{
|
||||
byte[] bytes = new byte[20];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
|
||||
{
|
||||
// # of 0's = length of pin
|
||||
const int Mod = 1000000;
|
||||
|
||||
// See https://tools.ietf.org/html/rfc4226
|
||||
// We can add an optional modifier
|
||||
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
|
||||
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));
|
||||
|
||||
// Generate DT string
|
||||
var offset = hash[hash.Length - 1] & 0xf;
|
||||
Debug.Assert(offset + 4 < hash.Length);
|
||||
var binaryCode = (hash[offset] & 0x7f) << 24
|
||||
| (hash[offset + 1] & 0xff) << 16
|
||||
| (hash[offset + 2] & 0xff) << 8
|
||||
| (hash[offset + 3] & 0xff);
|
||||
|
||||
return binaryCode % Mod;
|
||||
}
|
||||
|
||||
private static byte[] ApplyModifier(byte[] input, string modifier)
|
||||
{
|
||||
if (String.IsNullOrEmpty(modifier))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var modifierBytes = _encoding.GetBytes(modifier);
|
||||
var combined = new byte[checked(input.Length + modifierBytes.Length)];
|
||||
Buffer.BlockCopy(input, 0, combined, 0, input.Length);
|
||||
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// More info: https://tools.ietf.org/html/rfc6238#section-4
|
||||
private static ulong GetCurrentTimeStepNumber()
|
||||
{
|
||||
var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;
|
||||
return (ulong)(delta.Ticks / _timestep.Ticks);
|
||||
}
|
||||
|
||||
public static int GenerateCode(byte[] securityToken, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using (var hashAlgorithm = new HMACSHA1(securityToken))
|
||||
{
|
||||
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ValidateCode(byte[] securityToken, int code, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using (var hashAlgorithm = new HMACSHA1(securityToken))
|
||||
{
|
||||
for (var i = -2; i <= 2; i++)
|
||||
{
|
||||
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
|
||||
if (computedTotp == code)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Attributes;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Tokens
|
||||
{
|
||||
[Collection("Tokens")]
|
||||
public class SecurityToken : IZeroDbConventions
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Tokens
|
||||
{
|
||||
public class ZeroTokenProvider : IZeroTokenProvider
|
||||
{
|
||||
readonly char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-".ToCharArray();
|
||||
|
||||
readonly RandomNumberGenerator randonNumberGenerator;
|
||||
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
|
||||
public ZeroTokenProvider(IZeroStore store)
|
||||
{
|
||||
Store = store;
|
||||
randonNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default)
|
||||
{
|
||||
if (key.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new ArgumentException("Key cannot be empty");
|
||||
}
|
||||
if (length < 16)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Use at least a length of 16 for the token");
|
||||
}
|
||||
|
||||
string tokenKey = Random(length);
|
||||
|
||||
SecurityToken securityToken = new SecurityToken()
|
||||
{
|
||||
Id = TokenToId(tokenKey),
|
||||
Token = tokenKey,
|
||||
Key = HashKey(key),
|
||||
Metadata = metadata ?? new()
|
||||
};
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// saves the token
|
||||
await session.StoreAsync(securityToken);
|
||||
|
||||
// set the expires flag for the token
|
||||
IMetadataDictionary tokenMetadata = session.Advanced.GetMetadataFor(securityToken);
|
||||
tokenMetadata[Constants.Database.Expires] = DateTime.UtcNow.AddSeconds(expires.TotalSeconds);
|
||||
await session.SaveChangesAsync();
|
||||
|
||||
return tokenKey;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> Verify(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<SecurityToken> VerifyAndReturn(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
return securityToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Random(int length)
|
||||
{
|
||||
byte[] data = new byte[4 * length];
|
||||
randonNumberGenerator.GetBytes(data);
|
||||
|
||||
StringBuilder result = new(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var rnd = BitConverter.ToUInt32(data, i * 4);
|
||||
var idx = rnd % chars.Length;
|
||||
|
||||
result.Append(chars[idx]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts the token to a database id
|
||||
/// </summary>
|
||||
string TokenToId(string token)
|
||||
{
|
||||
string collection = Store.Raven.Conventions.GetCollectionName(typeof(SecurityToken));
|
||||
string idPrefix = Store.Raven.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(collection);
|
||||
return idPrefix + Store.Raven.Conventions.IdentityPartsSeparator + token;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hash for the security token key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
string HashKey(string key)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256;
|
||||
int iterationCount = 10000;
|
||||
int saltSize = 128 / 8;
|
||||
int numBytesRequested = 256 / 8;
|
||||
|
||||
byte[] salt = new byte[saltSize];
|
||||
randonNumberGenerator.GetBytes(salt);
|
||||
byte[] subkey = KeyDerivation.Pbkdf2(key, salt, prf, iterationCount, numBytesRequested);
|
||||
|
||||
var outputBytes = new byte[13 + salt.Length + subkey.Length];
|
||||
outputBytes[0] = 0x01;
|
||||
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
|
||||
WriteNetworkByteOrder(outputBytes, 5, (uint)iterationCount);
|
||||
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
|
||||
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
|
||||
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
|
||||
return Convert.ToBase64String(outputBytes);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the given key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
bool VerifyKey(string storedKey, string key)
|
||||
{
|
||||
byte[] decodedStoredKey = Convert.FromBase64String(storedKey);
|
||||
|
||||
try
|
||||
{
|
||||
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedStoredKey, 1);
|
||||
int embeddedIterCount = (int)ReadNetworkByteOrder(decodedStoredKey, 5);
|
||||
int saltLength = (int)ReadNetworkByteOrder(decodedStoredKey, 9);
|
||||
|
||||
// Read the salt: must be >= 128 bits
|
||||
if (saltLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] salt = new byte[saltLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13, salt, 0, salt.Length);
|
||||
|
||||
// Read the subkey (the rest of the payload): must be >= 128 bits
|
||||
int subkeyLength = decodedStoredKey.Length - 13 - salt.Length;
|
||||
if (subkeyLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
byte[] expectedSubkey = new byte[subkeyLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
|
||||
|
||||
// Hash the incoming key and verify it
|
||||
byte[] actualSubkey = KeyDerivation.Pbkdf2(key, salt, prf, embeddedIterCount, subkeyLength);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
|
||||
{
|
||||
buffer[offset + 0] = (byte)(value >> 24);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 3] = (byte)(value >> 0);
|
||||
}
|
||||
|
||||
static uint ReadNetworkByteOrder(byte[] buffer, int offset)
|
||||
{
|
||||
return ((uint)(buffer[offset + 0]) << 24)
|
||||
| ((uint)(buffer[offset + 1]) << 16)
|
||||
| ((uint)(buffer[offset + 2]) << 8)
|
||||
| ((uint)(buffer[offset + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroTokenProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a token for a <paramref name="key"/> with a specified <paramref name="expires"/> lifespan.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token will be used for. The key must match when verifying the token and should always be hidden from the user.</param>
|
||||
/// <param name="expires">The token will automatically expire after this timespan.</param>
|
||||
/// <param name="length">Length of the geneated token.</param>
|
||||
/// <param name="metadata">Additional metadata to store with the token. This data can be retrieved on verification with <see cref="VerifyAndReturn(string, string)"/></param>
|
||||
/// <returns>
|
||||
/// The generated token with the specified <paramref name="length"/>.
|
||||
/// </returns>
|
||||
Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// False, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<bool> Verify(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// Null, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<SecurityToken> VerifyAndReturn(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random token with the specified <paramref name="length"/> using the RNGCryptoServiceProvider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method won't store the token in the database and can't be used for verification. Use <see cref="Create(string, TimeSpan, int)"/> instead.
|
||||
/// </remarks>
|
||||
string Random(int length);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
global using zero;
|
||||
global using zero.Core;
|
||||
global using zero.Core.Api;
|
||||
global using zero.Core.Assemblies;
|
||||
global using zero.Core.Collections;
|
||||
global using zero.Core.Cultures;
|
||||
global using zero.Core.Database;
|
||||
global using zero.Core.Entities;
|
||||
global using zero.Core.Extensions;
|
||||
global using zero.Core.Handlers;
|
||||
global using zero.Core.Identity;
|
||||
global using zero.Core.Options;
|
||||
global using zero.Core.Plugins;
|
||||
global using zero.Core.Security;
|
||||
global using zero.Core.Validation;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user