diff --git a/zero.Backoffice/BackofficeAssetFileSystem.cs b/zero.Backoffice/BackofficeAssetFileSystem.cs new file mode 100644 index 00000000..c95279f4 --- /dev/null +++ b/zero.Backoffice/BackofficeAssetFileSystem.cs @@ -0,0 +1,11 @@ +namespace zero.Backoffice; + +public class BackofficeAssetFileSystem : PhysicalFileSystem, IBackofficeAssetFileSystem +{ + public BackofficeAssetFileSystem(string root) : base(root) { } +} + + +public interface IBackofficeAssetFileSystem : IFileSystem +{ +} \ No newline at end of file diff --git a/zero.Backoffice/Configuration/BackofficeOptions.cs b/zero.Backoffice/Configuration/BackofficeOptions.cs index ed5d84b0..5eafc93c 100644 --- a/zero.Backoffice/Configuration/BackofficeOptions.cs +++ b/zero.Backoffice/Configuration/BackofficeOptions.cs @@ -2,6 +2,11 @@ public class BackofficeOptions { + /// + /// Public path to zero assets + /// + public string AssetPath { get; set; } + /// /// Paths in the backoffice which are not handled by zero /// diff --git a/zero.Backoffice/Endpoints/DebugController.cs b/zero.Backoffice/Endpoints/DebugController.cs new file mode 100644 index 00000000..b9822db8 --- /dev/null +++ b/zero.Backoffice/Endpoints/DebugController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using zero.Api.Filters; +using zero.Backoffice.Services; +using zero.Identity.Models; + +namespace zero.Backoffice; + +[ZeroSystemApi] +[ZeroAuthorize(false)] +public class DebugController : ZeroBackofficeController +{ + [HttpGet("icons")] + public async Task GetIcons([FromServices] IIconRepository icons) + { + return Ok(await icons.GetSets()); + } +} \ No newline at end of file diff --git a/zero.Backoffice/Plugin.cs b/zero.Backoffice/Plugin.cs index ec438e67..a9e2667a 100644 --- a/zero.Backoffice/Plugin.cs +++ b/zero.Backoffice/Plugin.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System.IO; using zero.Backoffice.Endpoints.Account; +using zero.Backoffice.Services; namespace zero.Backoffice; @@ -29,6 +30,15 @@ public class ZeroBackofficePlugin : ZeroPlugin services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(svc => + { + IOptions options = svc.GetRequiredService>(); + IWebHostEnvironment env = svc.GetRequiredService(); + return new(Path.Combine(env.WebRootPath, options.Value.AssetPath)); + }); + services.AddZeroBackofficeUIComposition(); //services.AddTransient(); @@ -43,13 +53,16 @@ public class ZeroBackofficePlugin : ZeroPlugin protected void ConfigureOptions(BackofficeOptions options, IWebHostEnvironment env) { + options.ExcludedPaths.Add("resources"); + options.AssetPath = "zero/resources"; + options.DevServer.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App"); options.IconSets.Add(new BackofficeIconSet() { Alias = "feather", Name = "Feather", - SpritePath = "/assets/icons/feather.svg", + SpritePath = "icons/feather.svg", Prefix = "fth" }); diff --git a/zero.Backoffice/Resources/index.tpl.html b/zero.Backoffice/Resources/backoffice.tpl.html similarity index 100% rename from zero.Backoffice/Resources/index.tpl.html rename to zero.Backoffice/Resources/backoffice.tpl.html diff --git a/zero.Backoffice/Resources/login.tpl.html b/zero.Backoffice/Resources/login.tpl.html new file mode 100644 index 00000000..8a9e63ec --- /dev/null +++ b/zero.Backoffice/Resources/login.tpl.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + {css} + welcome to zero + + + +
+ {svg} + {js} + + + \ No newline at end of file diff --git a/zero.Backoffice/Services/Icons/BackofficeIconSetPresentation.cs b/zero.Backoffice/Services/Icons/BackofficeIconSetPresentation.cs new file mode 100644 index 00000000..9cb0efb0 --- /dev/null +++ b/zero.Backoffice/Services/Icons/BackofficeIconSetPresentation.cs @@ -0,0 +1,12 @@ +namespace zero.Backoffice.Services; + +public class BackofficeIconSetPresentation +{ + public string Alias { get; set; } + + public string Name { get; set; } + + public string Prefix { get; set; } + + public IEnumerable Icons { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Services/Icons/IconRepository.cs b/zero.Backoffice/Services/Icons/IconRepository.cs new file mode 100644 index 00000000..1f72cfd1 --- /dev/null +++ b/zero.Backoffice/Services/Icons/IconRepository.cs @@ -0,0 +1,113 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace zero.Backoffice.Services; + +public class IconRepository : IIconRepository +{ + protected IOptions Options { get; set; } + + protected IBackofficeAssetFileSystem FileSystem { get; set; } + + protected ILogger Logger { get; set; } + + protected ConcurrentBag CachedSets { get; private set; } = new(); + + protected string CachedSvg { get; set; } + + protected bool IsLoaded { get; set; } + + + public IconRepository(IOptions options, IBackofficeAssetFileSystem fileSystem, ILogger logger) + { + Options = options; + FileSystem = fileSystem; + Logger = logger; + } + + + /// + public async Task> GetSets() + { + await EnsureIconsAreLoaded(); + return CachedSets; + } + + + /// + public async Task GetCompiledSvg() + { + await EnsureIconsAreLoaded(); + return CachedSvg; + } + + + async Task EnsureIconsAreLoaded() + { + if (IsLoaded) + { + return; + } + + StringBuilder svg = new(); + + foreach (BackofficeIconSet set in Options.Value.IconSets) + { + if (!await FileSystem.Exists(set.SpritePath)) + { + Logger.LogWarning("Could not load icon set {alias} from path {path}", set.Alias, FileSystem.Map(set.SpritePath)); + continue; + } + + using Stream stream = await FileSystem.StreamFile(set.SpritePath); + + XDocument xml = XDocument.Load(stream); + IEnumerable symbols = xml.Descendants().Where(x => x.Name.LocalName == "symbol"); + + if (!symbols.Any()) + { + Logger.LogWarning("Icon set {alias} does not contain any ", set.Alias); + continue; + } + + HashSet icons = new(); + + foreach (XElement symbol in symbols) + { + string symbolAlias = set.Prefix + "-" + symbol.Attribute("id").Value.ToString(); + symbol.SetAttributeValue("id", symbolAlias); + svg.Append(symbol.ToString().RemoveNewLines()); + icons.Add(symbolAlias); + } + + CachedSets.Add(new() + { + Alias = set.Alias, + Name = set.Name, + Prefix = set.Prefix, + Icons = icons + }); + } + + CachedSvg = svg.ToString(); + IsLoaded = true; + } +} + + +public interface IIconRepository +{ + /// + /// Get all registered icon sets with all their containing icons + /// + Task> GetSets(); + + /// + /// Get compiled SVG string for alle registered icon sets + /// + Task GetCompiledSvg(); +} \ No newline at end of file diff --git a/zero.Backoffice/ZeroIndexController.cs b/zero.Backoffice/ZeroIndexController.cs index 8d95b919..b285bff8 100644 --- a/zero.Backoffice/ZeroIndexController.cs +++ b/zero.Backoffice/ZeroIndexController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using System.IO; using zero.Api.Controllers; +using zero.Backoffice.Services; namespace zero.Backoffice.Controllers; @@ -10,13 +11,17 @@ public class ZeroIndexController : Controller { IZeroVue ZeroVue { get; set; } IZeroOptions Options { get; set; } + IAuthenticationService AuthService { get; set; } + IIconRepository IconRepository { get; set; } string[] JsDevModulePaths = new[] { "/vite/client", "/app/app.js" }; - public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options) + public ZeroIndexController(IZeroVue zeroVue, IZeroOptions options, IAuthenticationService authService, IIconRepository iconRepository) { ZeroVue = zeroVue; Options = options; + AuthService = authService; + IconRepository = iconRepository; } @@ -28,21 +33,18 @@ public class ZeroIndexController : Controller } string config = await ZeroVue.ConfigAsJson(); - + bool isLoggedIn = AuthService.IsLoggedIn(); int port = Options.For().DevServer.Port; - string resourceName = $"zero.Backoffice.Resources.index.tpl.html"; + string resourceName = $"zero.Backoffice.Resources.backoffice.tpl.html"; - using Stream stream = GetType().Assembly.GetManifestResourceStream(resourceName); - using StreamReader reader = new(stream); - - string template = await reader.ReadToEndAsync(); + string template = await LoadTemplate(resourceName); string content = TokenReplacement.Apply(template, new() { { "css", String.Empty }, { "js", String.Join(String.Empty, GetJsModules("http://localhost:" + port)) }, - { "svg", ZeroVue.GetIconSvg() }, + { "svg", await IconRepository.GetCompiledSvg() }, { "config", config } }); @@ -50,10 +52,36 @@ public class ZeroIndexController : Controller } + //async Task Login() + //{ + // string content = TokenReplacement.Apply(await LoadTemplate("zero.Backoffice.Resources.backoffice.tpl.html"), new() + // { + // { "css", String.Empty }, + // { "js", String.Join(String.Empty, GetJsModules("http://localhost:" + port)) }, + // { "svg", ZeroVue.GetIconSvg() }, + // { "config", config } + // }); + //} + + + //async Task Backoffice() + //{ + + //} + + IEnumerable GetJsModules(string domain) { return JsDevModulePaths.Select(path => $""); } + + + async Task LoadTemplate(string resourceName) + { + using Stream stream = GetType().Assembly.GetManifestResourceStream(resourceName); + using StreamReader reader = new(stream); + return await reader.ReadToEndAsync(); + } } public class ZeroBackofficeModel diff --git a/zero.Backoffice/ZeroVue.cs b/zero.Backoffice/ZeroVue.cs index 78979f7b..3506e90a 100644 --- a/zero.Backoffice/ZeroVue.cs +++ b/zero.Backoffice/ZeroVue.cs @@ -63,7 +63,6 @@ public class ZeroVue : IZeroVue config.Alias = CreateAliases(); config.AppId = Context.AppId; //config.SharedAppId = Constants.Database.SharedAppId; // TODO appx - config.Icons = CreateIconSets(); config.MultiApps = Options.For().EnableMultiple; ZeroUser user = await AuthenticationApi.GetUser(); @@ -349,57 +348,6 @@ public class ZeroVue : IZeroVue } - IList CreateIconSets() - { - List result = new(); - IEnumerable sets = Options.For().IconSets; - - StringBuilder svg = new(); - - foreach (BackofficeIconSet set in sets) - { - string path = Paths.Map(set.SpritePath.TrimStart('/')); - - if (!File.Exists(path)) - { - Logger.LogWarning("Could not load icon set {alias} from path {path}", set.Alias, path); - continue; - } - - string svgContent = File.ReadAllText(path, Encoding.UTF8); - XDocument xml = XDocument.Parse(svgContent); - IEnumerable symbols = xml.Descendants().Where(x => x.Name.LocalName == "symbol"); // ("symbol"); - - if (!symbols.Any()) - { - Logger.LogWarning("Icon set {alias} does not contain any ", set.Alias); - continue; - } - - ZeroVueIconSet iconSet = new() - { - Alias = set.Alias, - Name = set.Name, - Prefix = set.Prefix - }; - - foreach (XElement symbol in symbols) - { - string symbolAlias = set.Prefix + "-" + symbol.Attribute("id").Value.ToString(); - symbol.SetAttributeValue("id", symbolAlias); - svg.Append(symbol.ToString().RemoveNewLines()); - iconSet.Icons.Add(symbolAlias); - } - - result.Add(iconSet); - } - - IconSymbolsSvg = svg.ToString(); - - return result; - } - - List CreateBlueprints() { List items = new(); @@ -489,8 +437,6 @@ public class ZeroVueConfig public Dictionary Overrides { get; set; } = new Dictionary(); - public IList Icons { get; set; } = new List(); - public ZeroVueServices Services { get; set; } = new(); public IList Blueprints { get; set; } = new List(); @@ -558,17 +504,6 @@ public class ZeroVueApplication public string Image { get; set; } } -public class ZeroVueIconSet -{ - public string Alias { get; set; } - - public string Name { get; set; } - - public string Prefix { get; set; } - - public HashSet Icons { get; set; } = new(); -} - public class ZeroVueServices { public string YouTubeApiKey { get; set; } diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index 2e4abc41..deb7587a 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -18,7 +18,8 @@ - + + diff --git a/zero.Core/FileStorage/FileStorageModule.cs b/zero.Core/FileStorage/FileStorageModule.cs index 4ebca033..9e9fa4a9 100644 --- a/zero.Core/FileStorage/FileStorageModule.cs +++ b/zero.Core/FileStorage/FileStorageModule.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace zero.FileStorage; diff --git a/zero.Core/FileStorage/IFileSystem.cs b/zero.Core/FileStorage/IFileSystem.cs index b2a28a47..bd8c5238 100644 --- a/zero.Core/FileStorage/IFileSystem.cs +++ b/zero.Core/FileStorage/IFileSystem.cs @@ -4,6 +4,16 @@ namespace zero.FileStorage; public interface IFileSystem { + /// + /// Map a relative path to an absolute internal path + /// + string Map(string path); + + /// + /// Map an internal path to a public accessible path + /// + string MapToPublicPath(string path); + /// /// Determines whether a file or directory at the given path already exists /// diff --git a/zero.Core/FileStorage/PhysicalFileSystem.cs b/zero.Core/FileStorage/PhysicalFileSystem.cs index a633fc5c..c5b3cf33 100644 --- a/zero.Core/FileStorage/PhysicalFileSystem.cs +++ b/zero.Core/FileStorage/PhysicalFileSystem.cs @@ -14,6 +14,20 @@ public class PhysicalFileSystem : IFileSystem } + /// + public virtual string Map(string path) + { + return ResolvePath(path); + } + + + /// + public virtual string MapToPublicPath(string path) + { + return path.EnsureStartsWith('/'); + } + + /// public virtual Task Exists(string path, CancellationToken cancellationToken = default) { @@ -268,10 +282,11 @@ public class PhysicalFileSystem : IFileSystem path = path.Replace('\\', '/').FullTrim().Trim('/'); string physicalPath = Path.Combine(_root, path); + string rootPath = Path.GetFullPath(_root); // do not allow paths which are outside this file system // the boundaries are set be the basePath which is assigned in the file system constructor - if (!Path.GetFullPath(physicalPath).StartsWith(_root, StringComparison.InvariantCultureIgnoreCase)) + if (!Path.GetFullPath(physicalPath).StartsWith(rootPath, StringComparison.InvariantCultureIgnoreCase)) { throw new FileSystemException($"The path '{path}' is outside the file system"); } diff --git a/zero.Core/Media/MediaFileSystem.cs b/zero.Core/Media/MediaFileSystem.cs index 3b381485..05fdecb4 100644 --- a/zero.Core/Media/MediaFileSystem.cs +++ b/zero.Core/Media/MediaFileSystem.cs @@ -12,17 +12,11 @@ public class MediaFileSystem : PhysicalFileSystem, IMediaFileSystem /// - public virtual string MapToPublicPath(string path) + public override string MapToPublicPath(string path) { return PublicPathPrefix + path.TrimStart('/'); } } -public interface IMediaFileSystem : IFileSystem -{ - /// - /// Map an internal path to a public accessible path - /// - string MapToPublicPath(string path); -} \ No newline at end of file +public interface IMediaFileSystem : IFileSystem { } \ No newline at end of file