backoffice login loads! 👍
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero.Api.Endpoints.Pages;
|
||||
|
||||
public class PageBasic : BasicModel<Page>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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<string> Urls { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace zero.Api.Endpoints.Pages;
|
||||
|
||||
public class PageMapperProfile : ZeroMapperProfile
|
||||
{
|
||||
public override void Configure(IZeroMapper mapper)
|
||||
{
|
||||
mapper.Define<Page, PageBasic>((source, ctx) => new(), Map);
|
||||
// mapper.Define<CountrySave, Country>((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;
|
||||
//}
|
||||
}
|
||||
@@ -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<IList<ModuleType>> 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<Module> 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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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<IPermissionProvider, PagePermissions>();
|
||||
services.AddSingleton<IMapperProfile, PageMapperProfile>();
|
||||
|
||||
//services.Configure<RavenOptions>(opts =>
|
||||
//{
|
||||
// opts.Indexes.Add<zero_Api_MailTemplates_Listing>();
|
||||
//});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace zero.Api.Endpoints.Pages;
|
||||
|
||||
public class PagesController : ZeroApiTreeEntityStoreController<Page, IPagesStore>
|
||||
{
|
||||
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<ActionResult<PageEdit>> 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<ActionResult<PageEdit>> 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<ActionResult<Paged>> Get([FromQuery] ListQuery<Page> query)
|
||||
{
|
||||
query.SearchFor(entity => entity.Name);
|
||||
return GetModels<PageBasic>(query);
|
||||
}
|
||||
|
||||
|
||||
//[HttpPost("")]
|
||||
//[ZeroAuthorize(PagePermissions.Create)]
|
||||
//public virtual Task<ActionResult<Result>> Create(MailSave saveModel) => CreateModel<MailSave, MailEdit>(saveModel);
|
||||
|
||||
|
||||
//[HttpPut("{id}")]
|
||||
//[ZeroAuthorize(PagePermissions.Update)]
|
||||
//public virtual Task<ActionResult<Result>> Update(string id, MailSave updateModel, [FromQuery] string changeToken = null) => UpdateModel<MailSave, MailEdit>(id, updateModel, changeToken);
|
||||
|
||||
|
||||
//[HttpDelete("{id}")]
|
||||
//[ZeroAuthorize(PagePermissions.Delete)]
|
||||
//public virtual Task<ActionResult<Result>> Delete(string id) => DeleteModel(id);
|
||||
}
|
||||
@@ -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<Page, IPageService>
|
||||
// {
|
||||
// IRoutes Routes;
|
||||
|
||||
// public PagesController(IPageService collection, IRoutes routes) : base(collection)
|
||||
// {
|
||||
// Routes = routes;
|
||||
// }
|
||||
|
||||
|
||||
// public override async Task<EditModel<Page>> 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<Page, PageEditModel<Page>>(new PageEditModel<Page>()
|
||||
// {
|
||||
// Entity = entity,
|
||||
// PageType = Collection.GetPageType(entity.PageTypeAlias),
|
||||
// Urls = new List<string>() { await Routes.GetUrl(entity) }
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// public override async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids)
|
||||
// {
|
||||
// IReadOnlyCollection<PageType> pageTypes = Options.Pages.GetAllItems();
|
||||
// Dictionary<string, Page> pages = await Collection.GetByIds(ids.ToArray());
|
||||
// Dictionary<Page, Route> 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<IList<TreeItem>> 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<List<string>> GetUrls([FromQuery] string pageId)
|
||||
// {
|
||||
// string url = await Routes.GetUrl<Page>(pageId);
|
||||
// return url.HasValue() ? new List<string>() { url } : new List<string>();
|
||||
// }
|
||||
|
||||
|
||||
// public async Task<IList<PageType>> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent);
|
||||
|
||||
|
||||
// public async Task<EditModel<Page>> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent));
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<IList<Page>>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids);
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<Page>> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId);
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<Page>> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants);
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<string[]>> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants);
|
||||
|
||||
|
||||
// [HttpDelete]
|
||||
// public async Task<Result<string[]>> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true);
|
||||
// }
|
||||
//}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<IList<ModuleType>> 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<Module> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace zero.Backoffice.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// | 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}
|
||||
/// </summary>
|
||||
public class PagesController : ZeroBackofficeApiController
|
||||
{
|
||||
protected IPagesStore Store { get; set; }
|
||||
|
||||
public PagesController(IPagesStore store)
|
||||
{
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("empty")]
|
||||
public virtual async Task<Page> New()
|
||||
{
|
||||
return await Store.Empty();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public virtual async Task<Page> Get(string id, string changeVector = null)
|
||||
{
|
||||
return await Store.Load(id, changeVector);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public virtual async Task<Paged<Page>> Get(ListQuery<Page> query)
|
||||
{
|
||||
return await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.OrderByDescending(x => x.CreatedDate));
|
||||
}
|
||||
|
||||
//[HttpGet("{id}/revisions")]
|
||||
//public virtual async Task<List<Revision<Country>>> GetRevisions(string id)
|
||||
//{
|
||||
// return await Collection.GetRevisions(id);
|
||||
//}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public virtual async Task<Result<Page>> Create(Country model)
|
||||
{
|
||||
return await Store.Create(model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public virtual async Task<Result<Page>> Update(string id, Country model)
|
||||
{
|
||||
return await Store.Update(model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public virtual async Task<Result<Page>> Delete(string id)
|
||||
{
|
||||
return await Store.Delete(id);
|
||||
}
|
||||
}
|
||||
@@ -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<Page, IPageService>
|
||||
{
|
||||
IRoutes Routes;
|
||||
|
||||
public PagesController(IPageService collection, IRoutes routes) : base(collection)
|
||||
{
|
||||
Routes = routes;
|
||||
}
|
||||
|
||||
|
||||
public override async Task<EditModel<Page>> 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<Page, PageEditModel<Page>>(new PageEditModel<Page>()
|
||||
{
|
||||
Entity = entity,
|
||||
PageType = Collection.GetPageType(entity.PageTypeAlias),
|
||||
Urls = new List<string>() { await Routes.GetUrl(entity) }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public override async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids)
|
||||
{
|
||||
IReadOnlyCollection<PageType> pageTypes = Options.Pages.GetAllItems();
|
||||
Dictionary<string, Page> pages = await Collection.GetByIds(ids.ToArray());
|
||||
Dictionary<Page, Route> 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<IList<TreeItem>> 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<List<string>> GetUrls([FromQuery] string pageId)
|
||||
{
|
||||
string url = await Routes.GetUrl<Page>(pageId);
|
||||
return url.HasValue() ? new List<string>() { url } : new List<string>();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<PageType>> GetAllowedPageTypes([FromQuery] string parent = null) => await Collection.GetAllowedPageTypes(parent);
|
||||
|
||||
|
||||
public async Task<EditModel<Page>> GetEmptyByType([FromQuery] string type, [FromQuery] string parent = null) => Edit(await Collection.GetEmpty(type, parent));
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public async Task<Result<IList<Page>>> SaveSorting([FromBody] string[] ids) => await Collection.SaveSorting(ids);
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public async Task<Result<Page>> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId);
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public async Task<Result<Page>> Copy([FromBody] ActionCopyModel model) => await Collection.Copy(model.Id, model.DestinationId, model.IncludeDescendants);
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public async Task<Result<string[]>> Restore([FromBody] ActionCopyModel model) => await Collection.Restore(model.Id, model.IncludeDescendants);
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<Result<string[]>> DeleteRecursive([FromQuery] string id) => await Collection.Delete(id, recursive: true, moveToRecycleBin: true);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class ZeroApiPlugin : ZeroPlugin
|
||||
ZeroModuleCollection.AddModule<Endpoints.Search.SearchModule>(services, configuration);
|
||||
ZeroModuleCollection.AddModule<Endpoints.Translations.TranslationModule>(services, configuration);
|
||||
ZeroModuleCollection.AddModule<Endpoints.Mails.MailModule>(services, configuration);
|
||||
ZeroModuleCollection.AddModule<Endpoints.Pages.PageModule>(services, configuration);
|
||||
|
||||
PostConfigureServices?.Invoke(services, configuration);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ZeroApiControllerModelConvention : IControllerModelConvention
|
||||
/// <summary>
|
||||
/// Configure routing model for all backoffice controllers
|
||||
/// </summary>
|
||||
public void Apply(ControllerModel controller)
|
||||
public virtual void Apply(ControllerModel controller)
|
||||
{
|
||||
bool hasAttributeRouteModels = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ using Raven.Client.Documents.Indexes;
|
||||
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiController where TModel : ZeroEntity, new() where TStore : IEntityStore<TModel>
|
||||
public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiController
|
||||
where TModel : ZeroEntity, new()
|
||||
where TStore : IEntityStore<TModel>
|
||||
{
|
||||
protected TStore Store { get; set; }
|
||||
|
||||
@@ -21,9 +23,9 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<T>> EmptyModel<T>(string flavor) where T : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<T>> EmptyModel<T>(string flavorAlias = null) where T : DisplayModel<TModel>
|
||||
{
|
||||
TModel model = await Store.Empty(flavor);
|
||||
TModel model = await Store.Empty(flavorAlias);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApiEntityStoreController<TModel, TStore>
|
||||
where TModel : ZeroEntity, ISupportsTrees, new()
|
||||
where TStore : ITreeEntityStore<TModel>
|
||||
{
|
||||
public ZeroApiTreeEntityStoreController(TStore store) : base(store) { }
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetChildModels<T>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel>
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
Paged<TModel> result = await Store.LoadChildren(parentId, query.Page, query.PageSize, q => q.Filter(query));
|
||||
|
||||
return Mapper.Map<TModel, T>(result, (src, dest) =>
|
||||
{
|
||||
dest.Link = GetAction(src);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetChildModels<T, TIndex>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel> where TIndex : AbstractCommonApiForIndexes, new()
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
Paged<TModel> result = await Store.LoadChildren<TIndex>(parentId, query.Page, query.PageSize, q => q.Filter(query));
|
||||
|
||||
return Mapper.Map<TModel, T>(result, (src, dest) =>
|
||||
{
|
||||
dest.Link = GetAction(src);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> SortModels(string[] ids)
|
||||
{
|
||||
return await Store.Sort(ids);
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Move(pageId, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Copy(pageId, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.CopyWithDescendants(pageId, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> DeleteModelWithDescendants(string id)
|
||||
{
|
||||
TModel model = await Store.Load(id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
Result<string[]> result = await Store.DeleteWithDescendants(model);
|
||||
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
|
||||
async Task<ActionResult<Result>> PutOperation<TEdit>(Func<Task<Result<TModel>>> action) where TEdit : DisplayModel<TModel>
|
||||
{
|
||||
Result<TModel> result = await action();
|
||||
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
return Mapper.Map<TModel, TEdit>(result);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
<None Remove="Modules_legacy\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Endpoints\Pages\ModulesController.cs" />
|
||||
<Compile Remove="Endpoints\Pages\_PagesController.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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<BackofficeOptions>().DevServer.Port,
|
||||
Vue = ZeroVue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class ZeroBackofficeModel
|
||||
{
|
||||
public int Port { get; set; }
|
||||
|
||||
public IZeroVue Vue { get; set; }
|
||||
}
|
||||
@@ -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<TEntity, TCollection> : ZeroBackofficeController
|
||||
// where TEntity : ZeroIdEntity, new()
|
||||
// where TCollection : IStoreOperations<TEntity>
|
||||
//{
|
||||
// protected TCollection Collection { get; private set; }
|
||||
|
||||
// [Obsolete]
|
||||
// protected Func<IRavenQueryable<TEntity>, IRavenQueryable<TEntity>> DefaultQuery { get; set; }
|
||||
|
||||
// protected Action<TEntity, PickerPreviewModel> PreviewTransform { get; set; }
|
||||
|
||||
// protected Action<TEntity, PickerModel> PickerTransform { get; set; }
|
||||
|
||||
|
||||
// public ZeroBackofficeCollectionController(TCollection collection)
|
||||
// {
|
||||
// Collection = collection;
|
||||
// }
|
||||
|
||||
// public override void OnScopeChanged(string scope)
|
||||
// {
|
||||
// //Collection.ApplyScope(scope);
|
||||
// }
|
||||
|
||||
|
||||
// public virtual async Task<DisplayModel<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<DisplayModel<TEntity>> GetEmpty() => Edit(await Collection.Empty());
|
||||
|
||||
|
||||
// public virtual async Task<Paged<TEntity>> GetByQuery([FromQuery] ListQuery<TEntity> query)
|
||||
// {
|
||||
// return await Collection.Load(query);
|
||||
// }
|
||||
|
||||
|
||||
// public virtual async Task<Paged<Revision>> GetRevisions([FromQuery] string id, [FromQuery] ListQuery<TEntity> query)
|
||||
// {
|
||||
// return null; // TODO
|
||||
// //return await Collection.GetRevisions(id, query.Page, query.PageSize);
|
||||
// }
|
||||
|
||||
|
||||
// public virtual async Task<IEnumerable<PickerModel>> GetForPicker() => await SelectList(Collection.Stream(), PickerTransform);
|
||||
|
||||
|
||||
// public virtual async Task<IList<PickerPreviewModel>> GetPreviews([FromQuery] List<string> ids) => Previews(await Collection.Load(ids), PreviewTransform);
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public virtual async Task<Result<TEntity>> Save([FromBody] TEntity model) => await Collection.Save(model);
|
||||
|
||||
|
||||
// [HttpDelete]
|
||||
// public virtual async Task<Result<TEntity>> Delete([FromQuery] string id) => await Collection.Delete(id);
|
||||
//}
|
||||
@@ -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<ActionResult<UserModel>> GetUser()
|
||||
{
|
||||
ZeroUser user = await AuthService.GetUser();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("notfound");
|
||||
}
|
||||
|
||||
return Mapper.Map<ZeroUser, UserModel>(user);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("loggedin")]
|
||||
[ZeroAuthorize(false)]
|
||||
public ActionResult<bool> LoggedIn() => AuthService.IsLoggedIn();
|
||||
|
||||
|
||||
[HttpPost("login")]
|
||||
[ZeroAuthorize(false)]
|
||||
//[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> 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<ActionResult> Logout()
|
||||
{
|
||||
await AuthService.Logout();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
//[HttpPost, ZeroAuthorize]
|
||||
//public async Task<Result> SwitchApp(string appId)
|
||||
//{
|
||||
// return Result.Maybe(await Api.TrySwitchApp(appId));
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace zero.Backoffice.Endpoints.Account;
|
||||
|
||||
public class AccountMapperProfile : ZeroMapperProfile
|
||||
{
|
||||
public override void Configure(IZeroMapper mapper)
|
||||
{
|
||||
mapper.Define<ZeroUser, UserModel>((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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IBackofficeSection> Sections;
|
||||
readonly IEnumerable<ISettingsGroup> SettingsGroups;
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
@@ -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<BackofficeOptions>().Bind(configuration.GetSection("Zero:Backoffice")).Configure<IWebHostEnvironment>(ConfigureOptions);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
|
||||
services.AddHostedService<ZeroDevService>();
|
||||
services.AddTransient<IZeroVue, ZeroVue>();
|
||||
services.AddSingleton<IMapperProfile, AccountMapperProfile>();
|
||||
|
||||
services.AddZeroBackofficeUIComposition();
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta name="author" content="brothers">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="expires" content="0">
|
||||
<link rel="icon" type="image/png" href="/assets/zero-icon.png" sizes="32x32">
|
||||
<link rel="image_src" type="image/png" href="/assets/zero-icon.png" sizes="192x192" />
|
||||
<link rel="shortcut icon" type="image/png" href="/assets/zero-icon.png" sizes="196x196">
|
||||
<base href="~/">
|
||||
{css}
|
||||
<title>zero</title>
|
||||
</head>
|
||||
|
||||
<body class="theme-light theme-rounded">
|
||||
<div id="app"></div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">{svg}</svg>
|
||||
|
||||
<script id="zeroconfig" type="application/json">{config}</script>
|
||||
<script>
|
||||
window.__zero = JSON.parse(document.getElementById('zeroconfig').innerHTML);
|
||||
window.zero = window.__zero;
|
||||
</script>
|
||||
|
||||
{js}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta name="author" content="brothers">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="expires" content="0">
|
||||
<link rel="icon" type="image/png" href="/assets/zero-icon.png" sizes="32x32">
|
||||
<link rel="image_src" type="image/png" href="/assets/zero-icon.png" sizes="192x192" />
|
||||
<link rel="shortcut icon" type="image/png" href="/assets/zero-icon.png" sizes="196x196">
|
||||
<base href="~/">
|
||||
{{styles}}
|
||||
<title>zero</title>
|
||||
</head>
|
||||
<body class="theme-light theme-roundedx">
|
||||
<div id="app"></div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">{{svg}}</svg>
|
||||
<script id="zeroconfig" type="application/json">{{config}}</script>
|
||||
<script>
|
||||
window.__zero = {{config}};
|
||||
window.zero = window.__zero;
|
||||
</script>
|
||||
{{scripts}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Configure routing model for all backoffice controllers
|
||||
/// </summary>
|
||||
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<ZeroSystemApiAttribute>() == null;
|
||||
|
||||
controller.Selectors[0].AttributeRouteModel = isAppAware ? AppAwareRouteModel : AppUnawareRouteModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
@@ -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<ActionResult> Index()
|
||||
{
|
||||
if (Options.Version.IsNullOrEmpty())
|
||||
{
|
||||
return RedirectToAction("ZeroBackoffice", "Setup");
|
||||
}
|
||||
|
||||
string config = await ZeroVue.ConfigAsJson();
|
||||
|
||||
int port = Options.For<BackofficeOptions>().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<string> GetJsModules(string domain)
|
||||
{
|
||||
return JsDevModulePaths.Select(path => $"<script type='module' src='{domain.TrimEnd('/')}{path.EnsureStartsWith('/')}'></script>");
|
||||
}
|
||||
}
|
||||
|
||||
public class ZeroBackofficeModel
|
||||
{
|
||||
public int Port { get; set; }
|
||||
|
||||
public IZeroVue Vue { get; set; }
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
<None Remove="Modules_legacy\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\index.tpl.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -27,6 +27,18 @@ public class SchemedSignInManager<TUser> : SignInManager<TUser> where TUser : Ze
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> CanSignInAsync(TUser user)
|
||||
{
|
||||
if (!user.IsActive)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
return base.CanSignInAsync(user);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsSignedIn(ClaimsPrincipal principal)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using zero.Identity.Models;
|
||||
|
||||
namespace zero.Identity;
|
||||
|
||||
@@ -54,50 +55,29 @@ public class AuthenticationService : IAuthenticationService
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> Login(string email, string password, bool isPersistent)
|
||||
public async Task<LoginResult> 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
|
||||
/// <summary>
|
||||
/// Logs a zero-user in and sets cookie
|
||||
/// </summary>
|
||||
Task<Result> Login(string email, string password, bool isPersistent);
|
||||
Task<LoginResult> Login(string email, string password, bool isPersistent);
|
||||
|
||||
/// <summary>
|
||||
/// Logs out the current user
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
protected virtual Translation LoadTranslation(string key)
|
||||
{
|
||||
return Store.Session().Query<Translation>().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^
|
||||
return Store.Session().Synchronous.Query<Translation>().FirstOrDefault(x => x.Key == key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -35,7 +35,7 @@ public class PageTypeService : IPageTypeService
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<FlavorConfig>> GetAllowedFlavors(string pageParentId = null)
|
||||
public async Task<IEnumerable<FlavorConfig>> GetAllowedTypes(string pageParentId = null)
|
||||
{
|
||||
List<Page> parents = new();
|
||||
|
||||
@@ -80,7 +80,7 @@ public interface IPageTypeService
|
||||
/// <summary>
|
||||
/// Get all page types which are allowed below a selected parent page
|
||||
/// </summary>
|
||||
Task<IEnumerable<FlavorConfig>> GetAllowedFlavors(string pageParentId = null);
|
||||
Task<IEnumerable<FlavorConfig>> GetAllowedTypes(string pageParentId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific page type by alias
|
||||
|
||||
@@ -16,7 +16,7 @@ public class PagesStore : TreeEntityStore<Page>, IPagesStore
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> IsAllowedAsChild(Page model, string parentId)
|
||||
{
|
||||
IEnumerable<FlavorConfig> pageTypes = await PageTypes.GetAllowedFlavors(parentId);
|
||||
IEnumerable<FlavorConfig> pageTypes = await PageTypes.GetAllowedTypes(parentId);
|
||||
return pageTypes.Any(x => x.Alias == model.Flavor);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ public class PagesStore : TreeEntityStore<Page>, 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))
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// A change token holds a reference to a database entity
|
||||
/// This is used to verify change requests for entities in the zero backoffice
|
||||
/// </summary>
|
||||
public class ChangeToken : ISupportsDbConventions
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string ReferenceId { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -15,7 +15,16 @@ public class ZeroStore : IZeroStore
|
||||
protected Dictionary<string, IZeroDocumentSession> ScopedSessions { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ResolvedDatabase { get; set; }
|
||||
string _resolvedDatabase = null;
|
||||
public string ResolvedDatabase
|
||||
{
|
||||
get => _resolvedDatabase;
|
||||
set
|
||||
{
|
||||
_resolvedDatabase = value;
|
||||
Raven.ResolvedDatabase = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -33,10 +33,10 @@ public abstract class EntityStore<T> : IEntityStore<T> where T : ZeroIdEntity, I
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<T> Empty(string flavor = null) => Operations.Empty<T>(flavor);
|
||||
public virtual Task<T> Empty(string flavorAlias = null) => Operations.Empty<T>(flavorAlias);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<TFlavor> Empty<TFlavor>(string flavor) where TFlavor : T, new() => Operations.Empty<T, TFlavor>(flavor);
|
||||
public virtual Task<TFlavor> Empty<TFlavor>(string flavorAlias = null) where TFlavor : T, new() => Operations.Empty<T, TFlavor>(flavorAlias);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<T> Load(string id, string changeVector = null) => Operations.Load<T>(id, changeVector);
|
||||
@@ -106,12 +106,12 @@ public interface IEntityStore<T> where T : ZeroIdEntity, new()
|
||||
/// <summary>
|
||||
/// Get new instance of an entity (with an optional flavor)
|
||||
/// </summary>
|
||||
Task<T> Empty(string flavor = null);
|
||||
Task<T> Empty(string flavorAlias = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get new instance of an entity with a specific flavor
|
||||
/// </summary>
|
||||
Task<TFlavor> Empty<TFlavor>(string flavor) where TFlavor : T, new();
|
||||
Task<TFlavor> Empty<TFlavor>(string flavorAlias = null) where TFlavor : T, new();
|
||||
|
||||
/// <summary>
|
||||
/// Get an entity by Id
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace zero.Stores;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace zero.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// A flavor config holds information about a flavor implementation
|
||||
@@ -8,6 +10,7 @@ public class FlavorConfig
|
||||
/// <summary>
|
||||
/// Type of the associated entity
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Type FlavorType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -31,6 +34,7 @@ public class FlavorConfig
|
||||
public string Icon { get; set; }
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<FlavorConfig, object> Construct { get; set; }
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public class FlavorOptions
|
||||
return provider.DefaultFlavor;
|
||||
}
|
||||
|
||||
return provider.Flavors.First().Alias;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,28 +3,35 @@
|
||||
public partial class StoreOperations : IStoreOperations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual Task<T> Empty<T>(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty<T, T>(flavor);
|
||||
public virtual Task<T> Empty<T>(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new() => Empty<T, T>(flavorAlias);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<TFlavor> Empty<T, TFlavor>(string flavor)
|
||||
public virtual Task<TFlavor> Empty<T, TFlavor>(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<T>())
|
||||
if (flavorAlias.IsNullOrEmpty() && !Flavors.CanUseWithoutFlavors<T>())
|
||||
{
|
||||
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<T>();
|
||||
|
||||
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<TFlavor>(new());
|
||||
}
|
||||
|
||||
// try to load and construct a specific flavor
|
||||
FlavorConfig config = Flavors.Get<T>(flavor);
|
||||
FlavorConfig config = Flavors.Get<T>(flavorAlias);
|
||||
TFlavor result = config?.Construct(config) as TFlavor;
|
||||
|
||||
if (result == null)
|
||||
@@ -32,7 +39,7 @@ public partial class StoreOperations : IStoreOperations
|
||||
return Task.FromResult<TFlavor>(default);
|
||||
}
|
||||
|
||||
result.Flavor = flavor;
|
||||
result.Flavor = flavorAlias;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -114,12 +114,13 @@ public interface IStoreOperations
|
||||
/// <summary>
|
||||
/// Get new instance of an entity (with an optional flavor)
|
||||
/// </summary>
|
||||
Task<T> Empty<T>(string flavor = null) where T : ZeroIdEntity, ISupportsFlavors, new();
|
||||
Task<T> Empty<T>(string flavorAlias = null) where T : ZeroIdEntity, ISupportsFlavors, new();
|
||||
|
||||
/// <summary>
|
||||
/// Get new instance of an entity with a specific flavor
|
||||
/// </summary>
|
||||
Task<TFlavor> Empty<T, TFlavor>(string flavor)
|
||||
/// <param name="flavorAlias">Optional alias. If left out the default flavor is used (if configured)</param>
|
||||
Task<TFlavor> Empty<T, TFlavor>(string flavorAlias = null)
|
||||
where T : ZeroIdEntity, ISupportsFlavors, new()
|
||||
where TFlavor : T, new();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user