diff --git a/zero.Api/Configuration/ApiConstants.cs b/zero.Api/Configuration/ApiConstants.cs index 5fc59f63..83c4de95 100644 --- a/zero.Api/Configuration/ApiConstants.cs +++ b/zero.Api/Configuration/ApiConstants.cs @@ -11,5 +11,7 @@ public static class ApiConstants public const string IdNotFound = "Could not find persisted model for the given Id"; public const string ChangeTokenMismatch = "The change token is not valid anymore"; + + public const string ChildNotAllowed = "Not allowed as a child entity"; } } \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/Maps/PageBasic.cs b/zero.Api/Endpoints/Pages/Maps/PageBasic.cs new file mode 100644 index 00000000..590360d8 --- /dev/null +++ b/zero.Api/Endpoints/Pages/Maps/PageBasic.cs @@ -0,0 +1,6 @@ +namespace zero.Api.Endpoints.Pages; + +public class PageBasic : BasicModel +{ + +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/Maps/PageEdit.cs b/zero.Api/Endpoints/Pages/Maps/PageEdit.cs new file mode 100644 index 00000000..e4b02663 --- /dev/null +++ b/zero.Api/Endpoints/Pages/Maps/PageEdit.cs @@ -0,0 +1,13 @@ +namespace zero.Api.Endpoints.Pages; + +public class PageEdit : ZeroIdEntity +{ + // we can't use Page as property type here + // cause that would serialize flavors to Page and remove additional properties. + // according to the System.Text.Json docs using "object" will serialize to the implementing type + public object Page { get; set; } + + public FlavorConfig PageType { get; set; } + + public List Urls { get; set; } = new(); +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/Maps/PageMapperProfile.cs b/zero.Api/Endpoints/Pages/Maps/PageMapperProfile.cs new file mode 100644 index 00000000..3af51cc9 --- /dev/null +++ b/zero.Api/Endpoints/Pages/Maps/PageMapperProfile.cs @@ -0,0 +1,23 @@ +namespace zero.Api.Endpoints.Pages; + +public class PageMapperProfile : ZeroMapperProfile +{ + public override void Configure(IZeroMapper mapper) + { + mapper.Define((source, ctx) => new(), Map); + // mapper.Define((source, ctx) => new(), Map); + } + + + protected virtual void Map(Page source, PageBasic target, IZeroMapperContext ctx) + { + this.MapBasicData(source, target); + } + + //protected virtual void Map(CountrySave source, Country target, IZeroMapperContext ctx) + //{ + // this.MapSaveData(source, target); + // target.Code = source.Code; + // target.IsPreferred = source.IsPreferred; + //} +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/ModulesController.cs b/zero.Api/Endpoints/Pages/ModulesController.cs new file mode 100644 index 00000000..9752200d --- /dev/null +++ b/zero.Api/Endpoints/Pages/ModulesController.cs @@ -0,0 +1,40 @@ +//using Microsoft.AspNetCore.Mvc; +//using System; +//using System.Collections.Generic; +//using System.Threading.Tasks; +//using zero.Core.Api; +//using zero.Core.Entities; +//using zero.Core.Utils; +//using zero.Web.Models; + +//namespace zero.Web.Controllers +//{ +// public class ModulesController : BackofficeController +// { +// IModulesApi Api; + +// public ModulesController(IModulesApi api) +// { +// Api = api; +// } + + +// public async Task> GetModuleTypes([FromQuery] string[] tags = default, [FromQuery] string pageId = default) => await Api.GetModuleTypes(tags, pageId); + + +// public ModuleType GetModuleType([FromQuery] string alias) => Api.GetModuleType(alias); + + +// public EditModel GetEmpty(string alias) +// { +// ModuleType moduleType = Api.GetModuleType(alias); +// Module module = Activator.CreateInstance(moduleType.ContentType) as Module; + +// module.ModuleTypeAlias = moduleType.Alias; +// module.Id = IdGenerator.Create(8); +// module.IsActive = true; + +// return Edit(module); +// } +// } +//} diff --git a/zero.Api/Endpoints/Pages/PageModule.cs b/zero.Api/Endpoints/Pages/PageModule.cs new file mode 100644 index 00000000..02112f31 --- /dev/null +++ b/zero.Api/Endpoints/Pages/PageModule.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Api.Endpoints.Pages; + +public class PageModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + services.AddSingleton(); + + //services.Configure(opts => + //{ + // opts.Indexes.Add(); + //}); + } +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/PagePermissions.cs b/zero.Api/Endpoints/Pages/PagePermissions.cs new file mode 100644 index 00000000..01e9878a --- /dev/null +++ b/zero.Api/Endpoints/Pages/PagePermissions.cs @@ -0,0 +1,31 @@ +namespace zero.Api.Endpoints.Pages; + +public class PagePermissions : PermissionProvider +{ + public const string Group = "zero.page"; + + public const string Create = "zero.page.create"; + public const string Read = "zero.page.read"; + public const string Update = "zero.page.update"; + public const string Delete = "zero.page.delete"; + + + public override Task Configure(IPermissionContext context) + { + PermissionGroup group = new(Group, "@page.list"); + group.Permissions.Add(new Permission("zero.page.defaults", "Default permissions") + { + Children = new() + { + new(Create, "@permission.states.create"), + new(Read, "@permission.states.read"), + new(Update, "@permission.states.update"), + new(Delete, "@permission.states.delete") + } + }); + + context.AddGroup(group); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/PageTreeService.cs b/zero.Api/Endpoints/Pages/PageTreeService.cs index 313a59cb..25d849ea 100644 --- a/zero.Api/Endpoints/Pages/PageTreeService.cs +++ b/zero.Api/Endpoints/Pages/PageTreeService.cs @@ -4,6 +4,7 @@ using Raven.Client.Documents.Session; namespace zero.Api.Endpoints.Pages; +[Obsolete("Move to zero.Backoffice")] public class PageTreeService : IPageTreeService { protected IPagesStore Pages { get; private set; } diff --git a/zero.Api/Endpoints/Pages/PagesController.cs b/zero.Api/Endpoints/Pages/PagesController.cs new file mode 100644 index 00000000..3580baf0 --- /dev/null +++ b/zero.Api/Endpoints/Pages/PagesController.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; + +namespace zero.Api.Endpoints.Pages; + +public class PagesController : ZeroApiTreeEntityStoreController +{ + IPageTypeService PageTypeService; + IRoutes Routes; + + public PagesController(IPagesStore store, IPageTypeService pageTypeService, IRoutes routes) : base(store) + { + PageTypeService = pageTypeService; + Routes = routes; + } + + + [HttpGet("empty")] + [ZeroAuthorize(PagePermissions.Create)] + public virtual async Task> Empty(string flavor, string parentId = null) + { + Page page = await Store.Empty(flavor, parentId); + + if (page == null) + { + return NotFound(); + } + + if (!await Store.IsAllowedAsChild(page, parentId)) + { + return BadRequest(ApiConstants.HttpErrors.ChildNotAllowed); + } + + PageEdit model = new() + { + Id = page.Id, + Page = page, + PageType = PageTypeService.GetByAlias(page.Flavor) + }; + + return model; + } + + + [HttpGet("{id}")] + [ZeroAuthorize(PagePermissions.Read)] + public virtual async Task> Get(string id, string changeVector = null) + { + Page page = await Store.Load(id, changeVector); + + if (page == null) + { + return NotFound(); + } + + string url = await Routes.GetUrl(page); + + PageEdit model = new() + { + Id = page.Id, + Page = page, + PageType = PageTypeService.GetByAlias(page.Flavor), + Urls = url.HasValue() ? new() { url } : new() { } + }; + + return model; + } + + + [HttpGet("")] + [ZeroAuthorize(PagePermissions.Read)] + public virtual Task> Get([FromQuery] ListQuery query) + { + query.SearchFor(entity => entity.Name); + return GetModels(query); + } + + + //[HttpPost("")] + //[ZeroAuthorize(PagePermissions.Create)] + //public virtual Task> Create(MailSave saveModel) => CreateModel(saveModel); + + + //[HttpPut("{id}")] + //[ZeroAuthorize(PagePermissions.Update)] + //public virtual Task> Update(string id, MailSave updateModel, [FromQuery] string changeToken = null) => UpdateModel(id, updateModel, changeToken); + + + //[HttpDelete("{id}")] + //[ZeroAuthorize(PagePermissions.Delete)] + //public virtual Task> Delete(string id) => DeleteModel(id); +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Pages/_PagesController.cs b/zero.Api/Endpoints/Pages/_PagesController.cs new file mode 100644 index 00000000..3583d652 --- /dev/null +++ b/zero.Api/Endpoints/Pages/_PagesController.cs @@ -0,0 +1,104 @@ +//using Microsoft.AspNetCore.Mvc; +//using Raven.Client.Documents; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using zero.Core.Api; +//using zero.Core.Collections; +//using zero.Core.Entities; +//using zero.Core.Extensions; +//using zero.Core.Routing; +//using zero.Web.Models; + +//namespace zero.Web.Controllers +//{ +// public class PagesController : BackofficeCollectionController +// { +// IRoutes Routes; + +// public PagesController(IPageService collection, IRoutes routes) : base(collection) +// { +// Routes = routes; +// } + + +// public override async Task> GetById([FromQuery] string id, [FromQuery] string changeVector = null) +// { +// Page entity = changeVector.IsNullOrEmpty() ? await Collection.GetById(id) : await Collection.GetRevision(changeVector); + +// return entity == null ? null : Edit>(new PageEditModel() +// { +// Entity = entity, +// PageType = Collection.GetPageType(entity.PageTypeAlias), +// Urls = new List() { await Routes.GetUrl(entity) } +// }); +// } + + +// public override async Task> GetPreviews([FromQuery] List ids) +// { +// IReadOnlyCollection pageTypes = Options.Pages.GetAllItems(); +// Dictionary pages = await Collection.GetByIds(ids.ToArray()); +// Dictionary routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray()); + +// return Previews(pages, item => +// { +// routes.TryGetValue(item, out Route route); + +// PickerPreviewModel model = new() +// { +// Id = item.Id, +// Icon = pageTypes.FirstOrDefault(x => x.Alias == item.PageTypeAlias)?.Icon ?? "fth-folder", +// Name = item.Name, +// Text = route?.Url.Or("No URL found") +// }; + +// PreviewTransform?.Invoke(item, model); +// return model; +// }); + +// } + + +// public async Task> GetChildren([FromQuery] string parent = null, [FromQuery] string active = null, [FromQuery] string search = null) +// { +// return await Collection.GetChildren(parent, active, search); +// } + + +// public PageType GetPageType([FromQuery] string alias) => Collection.GetPageType(alias); + + +// public async Task> GetUrls([FromQuery] string pageId) +// { +// string url = await Routes.GetUrl(pageId); +// return url.HasValue() ? new List() { url } : new List(); +// } + + +// public async Task> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent); + + +// public async Task> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent)); + + +// [HttpPost] +// public async Task>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids); + + +// [HttpPost] +// public async Task> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId); + + +// [HttpPost] +// public async Task> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants); + + +// [HttpPost] +// public async Task> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants); + + +// [HttpDelete] +// public async Task> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true); +// } +//} diff --git a/zero.Api/Models/_legacy/LoginModel.cs b/zero.Api/Models/_legacy/LoginModel.cs deleted file mode 100644 index 069b055e..00000000 --- a/zero.Api/Models/_legacy/LoginModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace zero.Web.Models -{ - public class LoginModel - { - public string Email { get; set; } - - public string Password { get; set; } - - public bool IsPersistent { get; set; } = true; - } -} diff --git a/zero.Api/Modules_legacy/Pages/ModulesController.cs b/zero.Api/Modules_legacy/Pages/ModulesController.cs deleted file mode 100644 index 29252a78..00000000 --- a/zero.Api/Modules_legacy/Pages/ModulesController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Api; -using zero.Core.Entities; -using zero.Core.Utils; -using zero.Web.Models; - -namespace zero.Web.Controllers -{ - public class ModulesController : BackofficeController - { - IModulesApi Api; - - public ModulesController(IModulesApi api) - { - Api = api; - } - - - public async Task> GetModuleTypes([FromQuery] string[] tags = default, [FromQuery] string pageId = default) => await Api.GetModuleTypes(tags, pageId); - - - public ModuleType GetModuleType([FromQuery] string alias) => Api.GetModuleType(alias); - - - public EditModel GetEmpty(string alias) - { - ModuleType moduleType = Api.GetModuleType(alias); - Module module = Activator.CreateInstance(moduleType.ContentType) as Module; - - module.ModuleTypeAlias = moduleType.Alias; - module.Id = IdGenerator.Create(8); - module.IsActive = true; - - return Edit(module); - } - } -} diff --git a/zero.Api/Modules_legacy/Pages/PagesController.cs b/zero.Api/Modules_legacy/Pages/PagesController.cs deleted file mode 100644 index 5a7a0d33..00000000 --- a/zero.Api/Modules_legacy/Pages/PagesController.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace zero.Backoffice.Modules; - -/// -/// | GET /zero/api/pages/empty -/// | GET /zero/api/pages/{id} -/// | GET /zero/api/pages/{id}/revisions -/// | GET /zero/api/pages[type,query,filter,...] -/// | POST /zero/api/pages -/// | PUT /zero/api/pages/{id} -/// | DELETE /zero/api/pages/{id} -/// -public class PagesController : ZeroBackofficeApiController -{ - protected IPagesStore Store { get; set; } - - public PagesController(IPagesStore store) - { - Store = store; - } - - - [HttpGet("empty")] - public virtual async Task New() - { - return await Store.Empty(); - } - - [HttpGet("{id}")] - public virtual async Task Get(string id, string changeVector = null) - { - return await Store.Load(id, changeVector); - } - - [HttpGet] - public virtual async Task> Get(ListQuery query) - { - return await Store.Load(query.Page, query.PageSize, q => q.OrderByDescending(x => x.CreatedDate)); - } - - //[HttpGet("{id}/revisions")] - //public virtual async Task>> GetRevisions(string id) - //{ - // return await Collection.GetRevisions(id); - //} - - - [HttpPost] - public virtual async Task> Create(Country model) - { - return await Store.Create(model); - } - - [HttpPut("{id}")] - public virtual async Task> Update(string id, Country model) - { - return await Store.Update(model); - } - - [HttpDelete("{id}")] - public virtual async Task> Delete(string id) - { - return await Store.Delete(id); - } -} \ No newline at end of file diff --git a/zero.Api/Modules_legacy/Pages/_PagesController.cs b/zero.Api/Modules_legacy/Pages/_PagesController.cs deleted file mode 100644 index 66577097..00000000 --- a/zero.Api/Modules_legacy/Pages/_PagesController.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Raven.Client.Documents; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Api; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Routing; -using zero.Web.Models; - -namespace zero.Web.Controllers -{ - public class PagesController : BackofficeCollectionController - { - IRoutes Routes; - - public PagesController(IPageService collection, IRoutes routes) : base(collection) - { - Routes = routes; - } - - - public override async Task> GetById([FromQuery] string id, [FromQuery] string changeVector = null) - { - Page entity = changeVector.IsNullOrEmpty() ? await Collection.GetById(id) : await Collection.GetRevision(changeVector); - - return entity == null ? null : Edit>(new PageEditModel() - { - Entity = entity, - PageType = Collection.GetPageType(entity.PageTypeAlias), - Urls = new List() { await Routes.GetUrl(entity) } - }); - } - - - public override async Task> GetPreviews([FromQuery] List ids) - { - IReadOnlyCollection pageTypes = Options.Pages.GetAllItems(); - Dictionary pages = await Collection.GetByIds(ids.ToArray()); - Dictionary routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray()); - - return Previews(pages, item => - { - routes.TryGetValue(item, out Route route); - - PickerPreviewModel model = new() - { - Id = item.Id, - Icon = pageTypes.FirstOrDefault(x => x.Alias == item.PageTypeAlias)?.Icon ?? "fth-folder", - Name = item.Name, - Text = route?.Url.Or("No URL found") - }; - - PreviewTransform?.Invoke(item, model); - return model; - }); - - } - - - public async Task> GetChildren([FromQuery] string parent = null, [FromQuery] string active = null, [FromQuery] string search = null) - { - return await Collection.GetChildren(parent, active, search); - } - - - public PageType GetPageType([FromQuery] string alias) => Collection.GetPageType(alias); - - - public async Task> GetUrls([FromQuery] string pageId) - { - string url = await Routes.GetUrl(pageId); - return url.HasValue() ? new List() { url } : new List(); - } - - - public async Task> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent); - - - public async Task> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent)); - - - [HttpPost] - public async Task>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids); - - - [HttpPost] - public async Task> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId); - - - [HttpPost] - public async Task> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants); - - - [HttpPost] - public async Task> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants); - - - [HttpDelete] - public async Task> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true); - } -} diff --git a/zero.Api/Plugin.cs b/zero.Api/Plugin.cs index d77eada1..dd5f1ed7 100644 --- a/zero.Api/Plugin.cs +++ b/zero.Api/Plugin.cs @@ -31,6 +31,7 @@ public class ZeroApiPlugin : ZeroPlugin ZeroModuleCollection.AddModule(services, configuration); ZeroModuleCollection.AddModule(services, configuration); ZeroModuleCollection.AddModule(services, configuration); + ZeroModuleCollection.AddModule(services, configuration); PostConfigureServices?.Invoke(services, configuration); } diff --git a/zero.Api/ZeroApiControllerModelConvention.cs b/zero.Api/ZeroApiControllerModelConvention.cs index 5d298033..7b79209b 100644 --- a/zero.Api/ZeroApiControllerModelConvention.cs +++ b/zero.Api/ZeroApiControllerModelConvention.cs @@ -27,7 +27,7 @@ public class ZeroApiControllerModelConvention : IControllerModelConvention /// /// Configure routing model for all backoffice controllers /// - public void Apply(ControllerModel controller) + public virtual void Apply(ControllerModel controller) { bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null); diff --git a/zero.Api/ZeroApiEntityStoreController.cs b/zero.Api/ZeroApiEntityStoreController.cs index 0ace4a01..52b306c1 100644 --- a/zero.Api/ZeroApiEntityStoreController.cs +++ b/zero.Api/ZeroApiEntityStoreController.cs @@ -3,7 +3,9 @@ using Raven.Client.Documents.Indexes; namespace zero.Api.Controllers; -public abstract class ZeroApiEntityStoreController : ZeroApiController where TModel : ZeroEntity, new() where TStore : IEntityStore +public abstract class ZeroApiEntityStoreController : ZeroApiController + where TModel : ZeroEntity, new() + where TStore : IEntityStore { protected TStore Store { get; set; } @@ -21,9 +23,9 @@ public abstract class ZeroApiEntityStoreController : ZeroApiCont } - protected async Task> EmptyModel(string flavor) where T : DisplayModel + protected async Task> EmptyModel(string flavorAlias = null) where T : DisplayModel { - TModel model = await Store.Empty(flavor); + TModel model = await Store.Empty(flavorAlias); if (model == null) { diff --git a/zero.Api/ZeroApiTreeEntityStoreController.cs b/zero.Api/ZeroApiTreeEntityStoreController.cs new file mode 100644 index 00000000..196db9d0 --- /dev/null +++ b/zero.Api/ZeroApiTreeEntityStoreController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using Raven.Client.Documents.Indexes; + +namespace zero.Api.Controllers; + +public abstract class ZeroApiTreeEntityStoreController : ZeroApiEntityStoreController + where TModel : ZeroEntity, ISupportsTrees, new() + where TStore : ITreeEntityStore +{ + public ZeroApiTreeEntityStoreController(TStore store) : base(store) { } + + + protected async Task> GetChildModels(string parentId, ListQuery query) where T : BasicModel + { + query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); + Paged result = await Store.LoadChildren(parentId, query.Page, query.PageSize, q => q.Filter(query)); + + return Mapper.Map(result, (src, dest) => + { + dest.Link = GetAction(src); + }); + } + + + protected async Task> GetChildModels(string parentId, ListQuery query) where T : BasicModel where TIndex : AbstractCommonApiForIndexes, new() + { + query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate); + Paged result = await Store.LoadChildren(parentId, query.Page, query.PageSize, q => q.Filter(query)); + + return Mapper.Map(result, (src, dest) => + { + dest.Link = GetAction(src); + }); + } + + + protected async Task> SortModels(string[] ids) + { + return await Store.Sort(ids); + } + + + protected async Task> MoveModel(string pageId, string newParentId) where TEdit : DisplayModel + { + return await PutOperation(async () => await Store.Move(pageId, newParentId)); + } + + + protected async Task> CopyModel(string pageId, string newParentId) where TEdit : DisplayModel + { + return await PutOperation(async () => await Store.Copy(pageId, newParentId)); + } + + + protected async Task> CopyModelWithDescendants(string pageId, string newParentId) where TEdit : DisplayModel + { + return await PutOperation(async () => await Store.CopyWithDescendants(pageId, newParentId)); + } + + + protected async Task> DeleteModelWithDescendants(string id) + { + TModel model = await Store.Load(id); + + if (model == null) + { + return NotFound(); + } + + Result result = await Store.DeleteWithDescendants(model); + + return result.WithoutModel(); + } + + + async Task> PutOperation(Func>> action) where TEdit : DisplayModel + { + Result result = await action(); + + if (Hints.ResponsePreference == ApiResponsePreference.Minimal) + { + return result.WithoutModel(); + } + + return Mapper.Map(result); + } +} diff --git a/zero.Api/zero.Api.csproj b/zero.Api/zero.Api.csproj index 1254f8b7..69b3e915 100644 --- a/zero.Api/zero.Api.csproj +++ b/zero.Api/zero.Api.csproj @@ -14,6 +14,11 @@ + + + + + diff --git a/zero.Backoffice/Controllers/ZeroIndexController.cs b/zero.Backoffice/Controllers/ZeroIndexController.cs deleted file mode 100644 index ad875c96..00000000 --- a/zero.Backoffice/Controllers/ZeroIndexController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using zero.Api.Controllers; - -namespace zero.Backoffice.Controllers; - -[ZeroAuthorize(false)] -[DisableBrowserCache] -public class ZeroIndexController : Controller -{ - IZeroVue ZeroVue { get; set; } - IZeroOptions Options { get; set; } - - public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options) - { - ZeroVue = zeroVue; - Options = options; - } - - - public IActionResult Index() - { - if (Options.Version.IsNullOrEmpty()) - { - return RedirectToAction("ZeroBackoffice", "Setup"); - } - - return View("Views/Zero/Index.cshtml", new ZeroBackofficeModel() - { - Port = Options.For().DevServer.Port, - Vue = ZeroVue - }); - } -} - -public class ZeroBackofficeModel -{ - public int Port { get; set; } - - public IZeroVue Vue { get; set; } -} diff --git a/zero.Backoffice/Controllers/_ZeroBackofficeCollectionController.cs b/zero.Backoffice/Controllers/_ZeroBackofficeCollectionController.cs deleted file mode 100644 index c2333c64..00000000 --- a/zero.Backoffice/Controllers/_ZeroBackofficeCollectionController.cs +++ /dev/null @@ -1,71 +0,0 @@ -//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 : ZeroBackofficeController -// where TEntity : ZeroIdEntity, new() -// where TCollection : IStoreOperations -//{ -// protected TCollection Collection { get; private set; } - -// [Obsolete] -// protected Func, IRavenQueryable> DefaultQuery { get; set; } - -// protected Action PreviewTransform { get; set; } - -// protected Action PickerTransform { get; set; } - - -// public ZeroBackofficeCollectionController(TCollection collection) -// { -// Collection = collection; -// } - -// public override void OnScopeChanged(string scope) -// { -// //Collection.ApplyScope(scope); -// } - - -// public virtual async Task> GetById([FromQuery] string id, [FromQuery] string changeVector = null) => Edit(await Collection.Load(id, changeVector)); - - -// public virtual async Task> GetByIds([FromQuery] string[] ids) => await Collection.Load(ids); - - -// public virtual async Task> GetEmpty() => Edit(await Collection.Empty()); - - -// public virtual async Task> GetByQuery([FromQuery] ListQuery query) -// { -// return await Collection.Load(query); -// } - - -// public virtual async Task> GetRevisions([FromQuery] string id, [FromQuery] ListQuery query) -// { -// return null; // TODO -// //return await Collection.GetRevisions(id, query.Page, query.PageSize); -// } - - -// public virtual async Task> GetForPicker() => await SelectList(Collection.Stream(), PickerTransform); - - -// public virtual async Task> GetPreviews([FromQuery] List ids) => Previews(await Collection.Load(ids), PreviewTransform); - - -// [HttpPost] -// public virtual async Task> Save([FromBody] TEntity model) => await Collection.Save(model); - - -// [HttpDelete] -// public virtual async Task> Delete([FromQuery] string id) => await Collection.Delete(id); -//} \ No newline at end of file diff --git a/zero.Backoffice/Endpoints/Account/AccountController.cs b/zero.Backoffice/Endpoints/Account/AccountController.cs new file mode 100644 index 00000000..33e0cab6 --- /dev/null +++ b/zero.Backoffice/Endpoints/Account/AccountController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc; +using zero.Api.Filters; +using zero.Identity.Models; + +namespace zero.Backoffice.Endpoints.Account; + +[ZeroSystemApi] +public class AccountController : ZeroBackofficeController +{ + readonly IAuthenticationService AuthService; + + public AccountController(IAuthenticationService authService) + { + AuthService = authService; + } + + + [HttpGet("user")] + public async Task> GetUser() + { + ZeroUser user = await AuthService.GetUser(); + + if (user == null) + { + return BadRequest("notfound"); + } + + return Mapper.Map(user); + } + + + [HttpGet("loggedin")] + [ZeroAuthorize(false)] + public ActionResult LoggedIn() => AuthService.IsLoggedIn(); + + + [HttpPost("login")] + [ZeroAuthorize(false)] + //[ValidateAntiForgeryToken] + public async Task Login(LoginModel model) + { + LoginResult result = await AuthService.Login(model.Email, model.Password, model.IsPersistent); + + if (result != LoginResult.Success) + { + return BadRequest(result.ToString()); // TODO translate resource + } + + return Ok(); + } + + + [HttpPost("logout")] + public async Task Logout() + { + await AuthService.Logout(); + return Ok(); + } + + + //[HttpPost, ZeroAuthorize] + //public async Task SwitchApp(string appId) + //{ + // return Result.Maybe(await Api.TrySwitchApp(appId)); + //} +} \ No newline at end of file diff --git a/zero.Backoffice/Endpoints/Account/AccountMapperProfile.cs b/zero.Backoffice/Endpoints/Account/AccountMapperProfile.cs new file mode 100644 index 00000000..051b7fa7 --- /dev/null +++ b/zero.Backoffice/Endpoints/Account/AccountMapperProfile.cs @@ -0,0 +1,23 @@ +namespace zero.Backoffice.Endpoints.Account; + +public class AccountMapperProfile : ZeroMapperProfile +{ + public override void Configure(IZeroMapper mapper) + { + mapper.Define((source, ctx) => new(), Map); + } + + + protected virtual void Map(ZeroUser source, UserModel target, IZeroMapperContext ctx) + { + target.Id = source.Id; + target.CurrentAppId = source.CurrentAppId; + target.IsSuper = source.IsSuper; + target.IsActive = source.IsActive; + target.AvatarId = source.AvatarId; + target.Name = source.Name; + target.Email = source.Email; + target.CreatedDate = source.CreatedDate; + target.Flavor = source.Flavor; + } +} \ No newline at end of file diff --git a/zero.Backoffice/Endpoints/Account/Models/LoginModel.cs b/zero.Backoffice/Endpoints/Account/Models/LoginModel.cs new file mode 100644 index 00000000..567612b4 --- /dev/null +++ b/zero.Backoffice/Endpoints/Account/Models/LoginModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace zero.Backoffice.Endpoints.Account; + +public class LoginModel +{ + [Required, EmailAddress] + public string Email { get; set; } + + [Required] + public string Password { get; set; } + + public bool IsPersistent { get; set; } = true; +} \ No newline at end of file diff --git a/zero.Backoffice/Endpoints/Account/Models/UserModel.cs b/zero.Backoffice/Endpoints/Account/Models/UserModel.cs new file mode 100644 index 00000000..327f4164 --- /dev/null +++ b/zero.Backoffice/Endpoints/Account/Models/UserModel.cs @@ -0,0 +1,20 @@ +namespace zero.Backoffice.Endpoints.Account; + +public class UserModel : ZeroIdEntity +{ + public string CurrentAppId { get; set; } + + public bool IsSuper { get; set; } + + public string AvatarId { get; set; } + + public string Email { get; set; } + + public string Name { get; set; } + + public bool IsActive { get; set; } + + public DateTimeOffset CreatedDate { get; set; } + + public string Flavor { get; set; } +} diff --git a/zero.Backoffice/Endpoints/Links/LinksController.cs b/zero.Backoffice/Endpoints/Links/LinksController.cs index 937dd0dc..72a11ed7 100644 --- a/zero.Backoffice/Endpoints/Links/LinksController.cs +++ b/zero.Backoffice/Endpoints/Links/LinksController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using zero.Api.Controllers; namespace zero.Backoffice.Endpoints.Links; -public class LinksController : ZeroApiController +public class LinksController : ZeroBackofficeController { readonly IZeroStore Store; readonly ILinks Links; diff --git a/zero.Backoffice/Endpoints/UI/UIController.cs b/zero.Backoffice/Endpoints/UI/UIController.cs index 08ef17fd..32e7105a 100644 --- a/zero.Backoffice/Endpoints/UI/UIController.cs +++ b/zero.Backoffice/Endpoints/UI/UIController.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Mvc; using System.Collections; -using zero.Api.Controllers; using zero.Api.Endpoints.Applications; namespace zero.Backoffice.Endpoints.UI; -public class UIController : ZeroApiController +public class UIController : ZeroBackofficeController { readonly IEnumerable Sections; readonly IEnumerable SettingsGroups; diff --git a/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs b/zero.Backoffice/Extensions/ZeroBackofficeControllerExtensions.cs similarity index 97% rename from zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs rename to zero.Backoffice/Extensions/ZeroBackofficeControllerExtensions.cs index e45da612..f6bb6bf2 100644 --- a/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs +++ b/zero.Backoffice/Extensions/ZeroBackofficeControllerExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.StaticFiles; using System.IO; using zero.Api.Controllers; -namespace zero.Backoffice.Controllers; +namespace zero.Backoffice.Extensions; public static class ZeroBackofficeControllerExtensions { diff --git a/zero.Backoffice/Plugin.cs b/zero.Backoffice/Plugin.cs index 32219644..ec438e67 100644 --- a/zero.Backoffice/Plugin.cs +++ b/zero.Backoffice/Plugin.cs @@ -2,7 +2,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using System.IO; +using zero.Backoffice.Endpoints.Account; namespace zero.Backoffice; @@ -21,8 +24,10 @@ public class ZeroBackofficePlugin : ZeroPlugin public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddOptions().Bind(configuration.GetSection("Zero:Backoffice")).Configure(ConfigureOptions); + services.TryAddEnumerable(ServiceDescriptor.Transient, ZeroBackofficeMvcOptions>()); services.AddHostedService(); services.AddTransient(); + services.AddSingleton(); services.AddZeroBackofficeUIComposition(); diff --git a/zero.Backoffice/Resources/Localization/zero.en-us.json b/zero.Backoffice/Resources/Localization/zero.en-us.json index c10ab0c7..3c168f00 100644 --- a/zero.Backoffice/Resources/Localization/zero.en-us.json +++ b/zero.Backoffice/Resources/Localization/zero.en-us.json @@ -405,6 +405,7 @@ "password_placeholder": "Enter your password" }, "errors": { + "unknown": "An unknown login error happened", "wrongcredentials": "Email or password is wrong", "lockedout": "The user has been locked out due to many failed password attempts", "disabled": "The user has been deactivated", @@ -725,46 +726,5 @@ "errors": { "cannotDeleteChild": "Entities which are synchronized with a blueprint cannot be deleted" } - }, - - "_test": { - "fields": { - "name": "Name", - "position": "Position", - "image": "Image", - "email": "Email", - "videoUri": "Video URL", - "isvisible": "Visible", - "facebook": "Facebook", - "facebook_text": "Link to facebook page", - "youtube": "Youtube", - "youtube_text": "Link to youtube channel", - "twitter": "Twitter", - "twitter_text": "Link to twitter profile", - "xIconPicker": "Icon", - "xTextarea": "Description", - "addresses": "Addresses", - "addresses_text": "Enter as many addresses as you wish", - "xCustom": "Generate", - "xRte": "Description as HTML", - "xMedia": "Images", - "xState": "Select media type", - "address": { - "city": "City", - "street": "Street", - "no": "No", - "countries": "Country", - "name": "Name", - "iso": "ISO Code" - }, - "hideInNavigation": "Hide in navigation", - "hideInTitle": "Hide in title", - "titleOverride": "Override title", - "titleOverrideAll": "Override full title", - "seoDescription": "SEO Description", - "seoImage": "Image", - "noFollow": "No follow", - "noIndex": "No index" - } } } \ No newline at end of file diff --git a/zero.Backoffice/Resources/index.tpl.html b/zero.Backoffice/Resources/index.tpl.html new file mode 100644 index 00000000..3401c4e7 --- /dev/null +++ b/zero.Backoffice/Resources/index.tpl.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + {css} + zero + + + +
+ {svg} + + + + + {js} + + + \ No newline at end of file diff --git a/zero.Backoffice/Views/index.tpl.html b/zero.Backoffice/Views/index.tpl.html deleted file mode 100644 index 8698a332..00000000 --- a/zero.Backoffice/Views/index.tpl.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - {{styles}} - zero - - -
- {{svg}} - - - {{scripts}} - - \ No newline at end of file diff --git a/zero.Backoffice/Controllers/ZeroBackofficeController.cs b/zero.Backoffice/ZeroBackofficeController.cs similarity index 100% rename from zero.Backoffice/Controllers/ZeroBackofficeController.cs rename to zero.Backoffice/ZeroBackofficeController.cs diff --git a/zero.Backoffice/ZeroBackofficeControllerModelConvention.cs b/zero.Backoffice/ZeroBackofficeControllerModelConvention.cs index 0e36ff3b..c0e06d6c 100644 --- a/zero.Backoffice/ZeroBackofficeControllerModelConvention.cs +++ b/zero.Backoffice/ZeroBackofficeControllerModelConvention.cs @@ -1,34 +1,41 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using System.Reflection; +using zero.Api.Controllers; +using zero.Api.Filters; namespace zero.Backoffice; -public class ZeroBackofficeControllerModelConvention : IApplicationModelConvention +public class ZeroBackofficeControllerModelConvention : ZeroApiControllerModelConvention { - readonly AttributeRouteModel RouteModel; + readonly AttributeRouteModel AppAwareRouteModel; + + readonly AttributeRouteModel AppUnawareRouteModel; readonly Type BaseClass = typeof(ZeroBackofficeController); + readonly bool RuntimeIsAppAware = false; - public ZeroBackofficeControllerModelConvention(string backofficePath) + + public ZeroBackofficeControllerModelConvention(string zeroPath, string backofficeApiPath = "backoffice", bool isAppAware = false) : base(zeroPath, backofficeApiPath, isAppAware) { - RouteModel = new AttributeRouteModel(new RouteAttribute(backofficePath.TrimEnd('/') + "/api/[controller]")); + RuntimeIsAppAware = isAppAware; + AppAwareRouteModel = BuildRouteModel(zeroPath, backofficeApiPath, true); + AppUnawareRouteModel = BuildRouteModel(zeroPath, backofficeApiPath, false); } /// /// Configure routing model for all backoffice controllers /// - public void Apply(ApplicationModel application) + public override void Apply(ControllerModel controller) { - foreach (var controller in application.Controllers) - { - bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null); + bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null); - if (controller.ControllerType.IsSubclassOf(BaseClass)) - { - controller.Selectors[0].AttributeRouteModel = RouteModel; - } + if (controller.ControllerType.IsSubclassOf(BaseClass)) + { + bool isAppAware = RuntimeIsAppAware && controller.ControllerType.GetCustomAttribute() == null; + + controller.Selectors[0].AttributeRouteModel = isAppAware ? AppAwareRouteModel : AppUnawareRouteModel; } } } \ No newline at end of file diff --git a/zero.Backoffice/ZeroBackofficeMvcOptions.cs b/zero.Backoffice/ZeroBackofficeMvcOptions.cs index a720074e..f620c628 100644 --- a/zero.Backoffice/ZeroBackofficeMvcOptions.cs +++ b/zero.Backoffice/ZeroBackofficeMvcOptions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace zero.Backoffice; diff --git a/zero.Backoffice/ZeroIndexController.cs b/zero.Backoffice/ZeroIndexController.cs new file mode 100644 index 00000000..8d95b919 --- /dev/null +++ b/zero.Backoffice/ZeroIndexController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using System.IO; +using zero.Api.Controllers; + +namespace zero.Backoffice.Controllers; + +[ZeroAuthorize(false)] +[DisableBrowserCache] +public class ZeroIndexController : Controller +{ + IZeroVue ZeroVue { get; set; } + IZeroOptions Options { get; set; } + + string[] JsDevModulePaths = new[] { "/vite/client", "/app/app.js" }; + + public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options) + { + ZeroVue = zeroVue; + Options = options; + } + + + public async Task Index() + { + if (Options.Version.IsNullOrEmpty()) + { + return RedirectToAction("ZeroBackoffice", "Setup"); + } + + string config = await ZeroVue.ConfigAsJson(); + + int port = Options.For().DevServer.Port; + + string resourceName = $"zero.Backoffice.Resources.index.tpl.html"; + + using Stream stream = GetType().Assembly.GetManifestResourceStream(resourceName); + using StreamReader reader = new(stream); + + string template = await reader.ReadToEndAsync(); + + string content = TokenReplacement.Apply(template, new() + { + { "css", String.Empty }, + { "js", String.Join(String.Empty, GetJsModules("http://localhost:" + port)) }, + { "svg", ZeroVue.GetIconSvg() }, + { "config", config } + }); + + return Content(content, "text/html"); + } + + + IEnumerable GetJsModules(string domain) + { + return JsDevModulePaths.Select(path => $""); + } +} + +public class ZeroBackofficeModel +{ + public int Port { get; set; } + + public IZeroVue Vue { get; set; } +} diff --git a/zero.Backoffice/Controllers/ZeroSetupController.cs b/zero.Backoffice/ZeroSetupController.cs similarity index 100% rename from zero.Backoffice/Controllers/ZeroSetupController.cs rename to zero.Backoffice/ZeroSetupController.cs diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index e54fae1b..2e4abc41 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/zero.Core/Identity/Models/LoginResult.cs b/zero.Core/Identity/Models/LoginResult.cs new file mode 100644 index 00000000..ed1be65f --- /dev/null +++ b/zero.Core/Identity/Models/LoginResult.cs @@ -0,0 +1,16 @@ +namespace zero.Identity.Models; + +public enum LoginResult +{ + [Localize("@login.errors.unknown")] + Unknown = 0, + [Localize("@login.errors.wrongcredentials")] + WrongCredentials = 1, + [Localize("@login.errors.lockedout")] + LockedOut = 2, + [Localize("@login.errors.notallowed")] + NotAllowed = 3, + [Localize("@login.errors.requirestwofactor")] + RequiresTwoFactor = 4, + Success = 10 +} \ No newline at end of file diff --git a/zero.Core/Identity/Security/SchemedSignInManager.cs b/zero.Core/Identity/Security/SchemedSignInManager.cs index d9c6f0c3..40030e67 100644 --- a/zero.Core/Identity/Security/SchemedSignInManager.cs +++ b/zero.Core/Identity/Security/SchemedSignInManager.cs @@ -27,6 +27,18 @@ public class SchemedSignInManager : SignInManager where TUser : Ze } + /// + public override Task CanSignInAsync(TUser user) + { + if (!user.IsActive) + { + return Task.FromResult(false); + } + + return base.CanSignInAsync(user); + } + + /// public override bool IsSignedIn(ClaimsPrincipal principal) { diff --git a/zero.Core/Identity/Services/AuthenticationService.cs b/zero.Core/Identity/Services/AuthenticationService.cs index ae2c34ff..9b2d810f 100644 --- a/zero.Core/Identity/Services/AuthenticationService.cs +++ b/zero.Core/Identity/Services/AuthenticationService.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using zero.Identity.Models; namespace zero.Identity; @@ -54,50 +55,29 @@ public class AuthenticationService : IAuthenticationService /// - public async Task Login(string email, string password, bool isPersistent) + public async Task Login(string email, string password, bool isPersistent) { - Result result = new(); - - ZeroUser user = await SignInManager.UserManager.FindByNameAsync(email); - - if (user == null) - { - result.AddError("@login.errors.wrongcredentials"); // TODO we don't need translations here, but return an enum, so the app itself can translate the error - return result; - } - // TODO probably move this logic into a custom SignInManager which overrides CanSignInAsync() - // see https://stackoverflow.com/a/35484758/670860 - else if (!user.IsActive) - { - result.AddError("@login.errors.disabled"); - return result; - } - - SignInResult signInResult = await SignInManager.PasswordSignInAsync(user, password, isPersistent, true); + SignInResult signInResult = await SignInManager.PasswordSignInAsync(email, password, isPersistent, true); if (!signInResult.Succeeded) { if (signInResult.IsLockedOut) { - result.AddError("@login.errors.lockedout"); + return LoginResult.LockedOut; } else if (signInResult.IsNotAllowed) { - result.AddError("@login.errors.notallowed"); + return LoginResult.NotAllowed; } else if (signInResult.RequiresTwoFactor) { - result.AddError("@login.errors.requirestwofactor"); + return LoginResult.RequiresTwoFactor; } - else - { - result.AddError("@login.errors.wrongcredentials"); - } - - return result; + + return LoginResult.WrongCredentials; } - return Result.Success(); + return LoginResult.Success; } @@ -141,7 +121,7 @@ public interface IAuthenticationService /// /// Logs a zero-user in and sets cookie /// - Task Login(string email, string password, bool isPersistent); + Task Login(string email, string password, bool isPersistent); /// /// Logs out the current user diff --git a/zero.Core/Localization/Localizer.cs b/zero.Core/Localization/Localizer.cs index 192c358b..bf2924ce 100644 --- a/zero.Core/Localization/Localizer.cs +++ b/zero.Core/Localization/Localizer.cs @@ -1,4 +1,6 @@ -using System.Reflection; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System.Reflection; namespace zero.Localization; @@ -8,7 +10,6 @@ public class Localizer : ILocalizer protected IZeroStore Store { get; private set; } - public Localizer(IZeroStore store) { Store = store; @@ -82,7 +83,7 @@ public class Localizer : ILocalizer /// protected virtual Translation LoadTranslation(string key) { - return Store.Session().Query().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^ + return Store.Session().Synchronous.Query().FirstOrDefault(x => x.Key == key); } } diff --git a/zero.Core/Mapper/ZeroMapper.cs b/zero.Core/Mapper/ZeroMapper.cs index 51098e64..9134182b 100644 --- a/zero.Core/Mapper/ZeroMapper.cs +++ b/zero.Core/Mapper/ZeroMapper.cs @@ -68,6 +68,12 @@ public class ZeroMapper : IZeroMapper } Type destinationType = typeof(TDestination); + + if (destinationType == source.GetType()) + { + return (TDestination)source; + } + ZeroMapperContext mapperContext = new(this); var constructor = GetConstructor(sourceType, destinationType); diff --git a/zero.Core/Pages/PageTypeService.cs b/zero.Core/Pages/PageTypeService.cs index 65c9ad6b..9b125c0e 100644 --- a/zero.Core/Pages/PageTypeService.cs +++ b/zero.Core/Pages/PageTypeService.cs @@ -35,7 +35,7 @@ public class PageTypeService : IPageTypeService /// - public async Task> GetAllowedFlavors(string pageParentId = null) + public async Task> GetAllowedTypes(string pageParentId = null) { List parents = new(); @@ -80,7 +80,7 @@ public interface IPageTypeService /// /// Get all page types which are allowed below a selected parent page /// - Task> GetAllowedFlavors(string pageParentId = null); + Task> GetAllowedTypes(string pageParentId = null); /// /// Get a specific page type by alias diff --git a/zero.Core/Pages/PagesStore.cs b/zero.Core/Pages/PagesStore.cs index 3268866f..3706b2f8 100644 --- a/zero.Core/Pages/PagesStore.cs +++ b/zero.Core/Pages/PagesStore.cs @@ -16,7 +16,7 @@ public class PagesStore : TreeEntityStore, IPagesStore /// public override async Task IsAllowedAsChild(Page model, string parentId) { - IEnumerable pageTypes = await PageTypes.GetAllowedFlavors(parentId); + IEnumerable pageTypes = await PageTypes.GetAllowedTypes(parentId); return pageTypes.Any(x => x.Alias == model.Flavor); } @@ -31,8 +31,7 @@ public class PagesStore : TreeEntityStore, IPagesStore return null; } - Page model = await Empty(); - model.Flavor = type.Alias; + Page model = await base.Empty(pageTypeAlias); model.ParentId = parentId; if (!await IsAllowedAsChild(model, parentId)) diff --git a/zero.Core/Persistence/Tokens/ChangeToken.cs b/zero.Core/Persistence/Tokens/ChangeToken.cs deleted file mode 100644 index ab5cb38d..00000000 --- a/zero.Core/Persistence/Tokens/ChangeToken.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace zero.Persistence; - -/// -/// A change token holds a reference to a database entity -/// This is used to verify change requests for entities in the zero backoffice -/// -public class ChangeToken : ISupportsDbConventions -{ - public string Id { get; set; } - - public string ReferenceId { get; set; } -} \ No newline at end of file diff --git a/zero.Core/Persistence/ZeroDocumentSession.cs b/zero.Core/Persistence/ZeroDocumentSession.cs index 19ffc8c3..20fda22f 100644 --- a/zero.Core/Persistence/ZeroDocumentSession.cs +++ b/zero.Core/Persistence/ZeroDocumentSession.cs @@ -7,6 +7,10 @@ public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession { public IZeroDocumentSession Core { get; private set; } + public IDocumentSession Synchronous => _synchronous ?? (_synchronous = DocumentStore.OpenSession(DatabaseName)); + + IDocumentSession _synchronous; + public ZeroDocumentSession(DocumentStore documentStore, Guid id, SessionOptions options, string coreDatabase = null) : base(documentStore, id, options) { if (coreDatabase.HasValue()) @@ -28,6 +32,12 @@ public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession } public bool IsDisposed { get; set; } + + public override void Dispose() + { + _synchronous?.Dispose(); + base.Dispose(); + } } @@ -35,5 +45,7 @@ public interface IZeroDocumentSession : IAsyncDocumentSession { IZeroDocumentSession Core { get; } + IDocumentSession Synchronous { get; } + bool IsDisposed { get; } } diff --git a/zero.Core/Persistence/ZeroStore.cs b/zero.Core/Persistence/ZeroStore.cs index 95881122..2444b732 100644 --- a/zero.Core/Persistence/ZeroStore.cs +++ b/zero.Core/Persistence/ZeroStore.cs @@ -15,7 +15,16 @@ public class ZeroStore : IZeroStore protected Dictionary ScopedSessions { get; set; } = new(); /// - public string ResolvedDatabase { get; set; } + string _resolvedDatabase = null; + public string ResolvedDatabase + { + get => _resolvedDatabase; + set + { + _resolvedDatabase = value; + Raven.ResolvedDatabase = value; + } + } /// diff --git a/zero.Core/Stores/EntityStore.cs b/zero.Core/Stores/EntityStore.cs index c16e0fa0..9c0efb51 100644 --- a/zero.Core/Stores/EntityStore.cs +++ b/zero.Core/Stores/EntityStore.cs @@ -33,10 +33,10 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I /// - public virtual Task Empty(string flavor = null) => Operations.Empty(flavor); + public virtual Task Empty(string flavorAlias = null) => Operations.Empty(flavorAlias); /// - public virtual Task Empty(string flavor) where TFlavor : T, new() => Operations.Empty(flavor); + public virtual Task Empty(string flavorAlias = null) where TFlavor : T, new() => Operations.Empty(flavorAlias); /// public virtual Task Load(string id, string changeVector = null) => Operations.Load(id, changeVector); @@ -106,12 +106,12 @@ public interface IEntityStore where T : ZeroIdEntity, new() /// /// Get new instance of an entity (with an optional flavor) /// - Task Empty(string flavor = null); + Task Empty(string flavorAlias = null); /// /// Get new instance of an entity with a specific flavor /// - Task Empty(string flavor) where TFlavor : T, new(); + Task Empty(string flavorAlias = null) where TFlavor : T, new(); /// /// Get an entity by Id diff --git a/zero.Core/Stores/Flavors/FlavorConfig.cs b/zero.Core/Stores/Flavors/FlavorConfig.cs index 06db8901..a0139203 100644 --- a/zero.Core/Stores/Flavors/FlavorConfig.cs +++ b/zero.Core/Stores/Flavors/FlavorConfig.cs @@ -1,4 +1,6 @@ -namespace zero.Stores; +using System.Text.Json.Serialization; + +namespace zero.Stores; /// /// A flavor config holds information about a flavor implementation @@ -8,6 +10,7 @@ public class FlavorConfig /// /// Type of the associated entity /// + [JsonIgnore] public Type FlavorType { get; private set; } /// @@ -31,6 +34,7 @@ public class FlavorConfig public string Icon { get; set; } + [JsonIgnore] public Func Construct { get; set; } diff --git a/zero.Core/Stores/Flavors/FlavorOptions.cs b/zero.Core/Stores/Flavors/FlavorOptions.cs index c00c068e..0ea9f8f5 100644 --- a/zero.Core/Stores/Flavors/FlavorOptions.cs +++ b/zero.Core/Stores/Flavors/FlavorOptions.cs @@ -34,7 +34,7 @@ public class FlavorOptions return provider.DefaultFlavor; } - return provider.Flavors.First().Alias; + return null; } diff --git a/zero.Core/Stores/StoreOperations.Empty.cs b/zero.Core/Stores/StoreOperations.Empty.cs index 6a320a12..56cc3820 100644 --- a/zero.Core/Stores/StoreOperations.Empty.cs +++ b/zero.Core/Stores/StoreOperations.Empty.cs @@ -3,28 +3,35 @@ public partial class StoreOperations : IStoreOperations { /// - public virtual Task Empty(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty(flavor); + public virtual Task Empty(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty(flavorAlias); /// - public virtual Task Empty(string flavor) + public virtual Task Empty(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new() where TFlavor : T, new() { // throw if this entity is not allowed to be created without a flavor - if (flavor.IsNullOrEmpty() && !Flavors.CanUseWithoutFlavors()) + if (flavorAlias.IsNullOrEmpty() && !Flavors.CanUseWithoutFlavors()) { - throw new ArgumentException("Can not create instance of an entity which is configured to to be only used as a flavor", nameof(flavor)); + string defaultFlavor = Flavors.DefaultFlavorFor(); + + if (defaultFlavor.IsNullOrEmpty()) + { + throw new ArgumentException("Can not create instance of an entity which is configured to to be only used as a flavor", nameof(flavorAlias)); + } + + flavorAlias = defaultFlavor; } // return default instance if no flavor is required - if (flavor.IsNullOrEmpty()) + if (flavorAlias.IsNullOrEmpty()) { return Task.FromResult(new()); } // try to load and construct a specific flavor - FlavorConfig config = Flavors.Get(flavor); + FlavorConfig config = Flavors.Get(flavorAlias); TFlavor result = config?.Construct(config) as TFlavor; if (result == null) @@ -32,7 +39,7 @@ public partial class StoreOperations : IStoreOperations return Task.FromResult(default); } - result.Flavor = flavor; + result.Flavor = flavorAlias; return Task.FromResult(result); } } \ No newline at end of file diff --git a/zero.Core/Stores/StoreOperations.cs b/zero.Core/Stores/StoreOperations.cs index 5b64424f..dc6800e2 100644 --- a/zero.Core/Stores/StoreOperations.cs +++ b/zero.Core/Stores/StoreOperations.cs @@ -114,12 +114,13 @@ public interface IStoreOperations /// /// Get new instance of an entity (with an optional flavor) /// - Task Empty(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new(); + Task Empty(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new(); /// /// Get new instance of an entity with a specific flavor /// - Task Empty(string flavor) + /// Optional alias. If left out the default flavor is used (if configured) + Task Empty(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new() where TFlavor : T, new();