From 98c2f012ee3de635a6972b1e49b4eb3057a9e6b5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 24 May 2023 13:09:10 -0400 Subject: [PATCH] ability to specify if a theme is enabled for a site --- .../Admin/ModuleDefinitions/Edit.razor | 2 +- .../Modules/Admin/Modules/Settings.razor | 58 +++---- Oqtane.Client/Modules/Admin/Pages/Add.razor | 12 +- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 12 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 8 +- Oqtane.Client/Modules/Admin/Themes/Edit.razor | 161 +++++++++++++++++ .../Modules/Admin/Themes/Index.razor | 13 +- Oqtane.Client/Modules/Admin/Themes/View.razor | 97 ----------- .../Admin/Themes/{View.resx => Edit.resx} | 9 + .../Resources/Modules/Admin/Themes/Index.resx | 3 + .../Services/Interfaces/IThemeService.cs | 15 ++ Oqtane.Client/Services/ThemeService.cs | 9 + .../Controllers/ModuleDefinitionController.cs | 2 +- Oqtane.Server/Controllers/ThemeController.cs | 47 ++++- .../EntityBuilders/ThemeEntityBuilder.cs | 36 ++++ .../Master/04000001_AddThemeTable.cs | 28 +++ .../Repository/Context/MasterDBContext.cs | 1 + .../Interfaces/IModuleDefinitionRepository.cs | 2 +- .../Repository/Interfaces/IThemeRepository.cs | 4 +- .../Repository/ModuleDefinitionRepository.cs | 52 +++--- Oqtane.Server/Repository/ThemeRepository.cs | 164 ++++++++++++++---- Oqtane.Shared/Models/Theme.cs | 59 ++++--- Oqtane.Shared/Shared/EntityNames.cs | 1 + 23 files changed, 564 insertions(+), 231 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/Themes/Edit.razor delete mode 100644 Oqtane.Client/Modules/Admin/Themes/View.razor rename Oqtane.Client/Resources/Modules/Admin/Themes/{View.resx => Edit.resx} (96%) create mode 100644 Oqtane.Server/Migrations/EntityBuilders/ThemeEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index 6d54d069..d030299c 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -308,7 +308,7 @@ { moduledefinition.Categories = _categories; } - moduledefinition.IsEnabled = (_isenabled == null ? true : Boolean.Parse(_isenabled)); + moduledefinition.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); moduledefinition.PermissionList = _permissionGrid.GetPermissionList(); await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition); await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition); diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 6767f011..a3133216 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -98,37 +98,35 @@ @code { - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; - public override string Title => "Module Settings"; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; + public override string Title => "Module Settings"; - private ElementReference form; - private bool validated = false; - private List _themes; - private List _containers = new List(); - private string _title; - private string _containerType; - private string _allPages = "false"; - private string _permissionNames = ""; - private List _permissions = null; - private string _pageId; - private PermissionGrid _permissionGrid; - private Type _moduleSettingsType; - private object _moduleSettings; - private string _moduleSettingsTitle = "Module Settings"; - private RenderFragment ModuleSettingsComponent { get; set; } - private Type _containerSettingsType; - private object _containerSettings; - private RenderFragment ContainerSettingsComponent { get; set; } - private string createdby; - private DateTime createdon; - private string modifiedby; - private DateTime modifiedon; + private ElementReference form; + private bool validated = false; + private List _containers = new List(); + private string _title; + private string _containerType; + private string _allPages = "false"; + private string _permissionNames = ""; + private List _permissions = null; + private string _pageId; + private PermissionGrid _permissionGrid; + private Type _moduleSettingsType; + private object _moduleSettings; + private string _moduleSettingsTitle = "Module Settings"; + private RenderFragment ModuleSettingsComponent { get; set; } + private Type _containerSettingsType; + private object _containerSettings; + private RenderFragment ContainerSettingsComponent { get; set; } + private string createdby; + private DateTime createdon; + private string modifiedby; + private DateTime modifiedon; - protected override async Task OnInitializedAsync() - { - _title = ModuleState.Title; - _themes = await ThemeService.GetThemesAsync(); - _containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType); + protected override void OnInitialized() + { + _title = ModuleState.Title; + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _containerType = ModuleState.ContainerType; _allPages = ModuleState.AllPages.ToString(); _permissions = ModuleState.PermissionList; @@ -173,7 +171,7 @@ AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error); } - var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); + var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType)) { _containerSettingsType = Type.GetType(theme.ContainerSettingsType); diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 13664992..9b3d709c 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -10,7 +10,7 @@
- @if (_themeList != null) + @if (PageState.Site.Themes != null) {
@@ -175,7 +175,6 @@ @code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; - private List _themeList; private List _themes = new List(); private List _containers = new List(); private string _name; @@ -207,10 +206,9 @@ { try { - _themeList = await ThemeService.GetThemesAsync(); - _themes = ThemeService.GetThemeControls(_themeList); + _themes = ThemeService.GetThemeControls(PageState.Site.Themes); _themetype = PageState.Site.DefaultThemeType; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = PageState.Site.DefaultContainerType; _children = PageState.Pages.Where(item => item.ParentId == null).ToList(); ThemeSettings(); @@ -262,7 +260,7 @@ try { _themetype = (string)e.Value; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = "-"; ThemeSettings(); StateHasChanged(); @@ -277,7 +275,7 @@ private void ThemeSettings() { _themeSettingsType = null; - var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); + var theme = PageState.Site.Themes.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) { _themeSettingsType = Type.GetType(theme.ThemeSettingsType); diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 0926bd22..57c26b99 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -11,7 +11,7 @@ - @if (_themeList != null) + @if (PageState.Site.Themes != null) {
@@ -210,7 +210,6 @@ private ElementReference form; private bool validated = false; - private List _themeList; private List _themes = new List(); private List _containers = new List(); private int _pageId; @@ -251,8 +250,7 @@ try { _children = PageState.Pages.Where(item => item.ParentId == null).ToList(); - _themeList = await ThemeService.GetThemesAsync(); - _themes = ThemeService.GetThemeControls(_themeList); + _themes = ThemeService.GetThemeControls(PageState.Site.Themes); _pageId = Int32.Parse(PageState.QueryString["id"]); page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId); @@ -294,7 +292,7 @@ { _themetype = PageState.Site.DefaultThemeType; } - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = page.DefaultContainerType; if (string.IsNullOrEmpty(_containertype)) { @@ -396,7 +394,7 @@ try { _themetype = (string)e.Value; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = "-"; ThemeSettings(); StateHasChanged(); @@ -413,7 +411,7 @@ _themeSettingsType = null; if (PageState.QueryString.ContainsKey("cp")) // can only be displayed if invoked from Control Panel { - var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); + var theme = PageState.Site.Themes.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) { _themeSettingsType = Type.GetType(theme.ThemeSettingsType); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 4111f3b6..c0f2df68 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -330,7 +330,6 @@ private ElementReference form; private bool validated = false; private bool _initialized = false; - private List _themeList; private List _themes = new List(); private List _containers = new List(); private string _name = string.Empty; @@ -383,7 +382,6 @@ { try { - _themeList = await ThemeService.GetThemesAsync(); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (site != null) { @@ -405,9 +403,9 @@ { _faviconfileid = site.FaviconFileId.Value; } - _themes = ThemeService.GetThemeControls(_themeList); + _themes = ThemeService.GetThemeControls(PageState.Site.Themes); _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; @@ -484,7 +482,7 @@ _themetype = (string)e.Value; if (_themetype != "-") { - _containers = ThemeService.GetContainerControls(_themeList, _themetype); + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); } else { diff --git a/Oqtane.Client/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor new file mode 100644 index 00000000..03bb6cf1 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -0,0 +1,161 @@ +@namespace Oqtane.Modules.Admin.Themes +@using System.Net +@inherits ModuleBase +@inject IThemeService ThemeService +@inject NavigationManager NavigationManager +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +@if (_initialized) +{ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + @SharedLocalizer["Cancel"] +
+
+ +} + +@code { + private bool _initialized = false; + private ElementReference form; + private bool validated = false; + private int _themeId; + private string _themeName = ""; + private string _isenabled; + private string _name; + private string _version; + private string _packagename; + private string _owner = ""; + private string _url = ""; + private string _contact = ""; + private string _license = ""; + private string _createdby; + private DateTime _createdon; + private string _modifiedby; + private DateTime _modifiedon; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; + + protected override async Task OnInitializedAsync() + { + try + { + _themeId = Int32.Parse(PageState.QueryString["id"]); + var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); + if (theme != null) + { + _name = theme.Name; + _isenabled =theme.IsEnabled.ToString(); + _version = theme.Version; + _packagename = theme.PackageName; + _owner = theme.Owner; + _url = theme.Url; + _contact = theme.Contact; + _license = theme.License; + _createdby = theme.CreatedBy; + _createdon = theme.CreatedOn; + _modifiedby = theme.ModifiedBy; + _modifiedon = theme.ModifiedOn; + + _initialized = true; + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Theme {ThemeName} {Error}", _themeName, ex.Message); + AddModuleMessage(Localizer["Error.Theme.Loading"], MessageType.Error); + } + } + + private async Task SaveTheme() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + try + { + var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); + theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); + await ThemeService.UpdateThemeAsync(theme); + await logger.LogInformation("Theme Saved {Theme}", theme); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving Theme {ThemeId} {Error}", _themeId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error); + } + } + else + { + AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } +} diff --git a/Oqtane.Client/Modules/Admin/Themes/Index.razor b/Oqtane.Client/Modules/Admin/Themes/Index.razor index 61b25d58..08895c8f 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Index.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Index.razor @@ -23,11 +23,12 @@ else   @SharedLocalizer["Name"] @SharedLocalizer["Version"] + @Localizer["Enabled"] @SharedLocalizer["Expires"]   - + @if (context.AssemblyName != Constants.ClientId) { @@ -36,6 +37,16 @@ else @context.Name @context.Version + + @if (context.IsEnabled) + { + @SharedLocalizer["Yes"] + } + else + { + @SharedLocalizer["No"] + } + @((MarkupString)PurchaseLink(context.PackageName)) diff --git a/Oqtane.Client/Modules/Admin/Themes/View.razor b/Oqtane.Client/Modules/Admin/Themes/View.razor deleted file mode 100644 index db38748b..00000000 --- a/Oqtane.Client/Modules/Admin/Themes/View.razor +++ /dev/null @@ -1,97 +0,0 @@ -@namespace Oqtane.Modules.Admin.Themes -@using System.Net -@inherits ModuleBase -@inject IThemeService ThemeService -@inject NavigationManager NavigationManager -@inject IStringLocalizer Localizer -@inject IStringLocalizer SharedLocalizer - -
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-@SharedLocalizer["Cancel"] - -@code { - private string _themeName = ""; - private string _name; - private string _version; - private string _packagename; - private string _owner = ""; - private string _url = ""; - private string _contact = ""; - private string _license = ""; - - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; - - protected override async Task OnInitializedAsync() - { - try - { - _themeName = WebUtility.UrlDecode(PageState.QueryString["name"]); - var themes = await ThemeService.GetThemesAsync(); - var theme = themes.FirstOrDefault(item => item.ThemeName == _themeName); - if (theme != null) - { - _name = theme.Name; - _version = theme.Version; - _packagename = theme.PackageName; - _owner = theme.Owner; - _url = theme.Url; - _contact = theme.Contact; - _license = theme.License; - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading Theme {ThemeName} {Error}", _themeName, ex.Message); - AddModuleMessage(Localizer["Error.Theme.Loading"], MessageType.Error); - } - } -} diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/View.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx similarity index 96% rename from Oqtane.Client/Resources/Modules/Admin/Themes/View.resx rename to Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index f950588f..4c3fd053 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/View.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -168,4 +168,13 @@ Package Name: + + Information + + + Is theme enabled for this site? + + + Enabled? + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx index 45824f4f..73d2a5ad 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx @@ -144,4 +144,7 @@ View + + Enabled? + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IThemeService.cs b/Oqtane.Client/Services/Interfaces/IThemeService.cs index 4d8d17c6..8c5ac457 100644 --- a/Oqtane.Client/Services/Interfaces/IThemeService.cs +++ b/Oqtane.Client/Services/Interfaces/IThemeService.cs @@ -16,6 +16,14 @@ namespace Oqtane.Services /// Task> GetThemesAsync(); + /// + /// Returns a specific thenme + /// + /// + /// + /// + Task GetThemeAsync(int themeId, int siteId); + /// /// Returns a list of s from the given themes /// @@ -39,6 +47,13 @@ namespace Oqtane.Services /// List GetContainerControls(List themes, string themeName); + /// + /// Updates a existing theem + /// + /// + /// + Task UpdateThemeAsync(Theme theme); + /// /// Deletes a theme /// diff --git a/Oqtane.Client/Services/ThemeService.cs b/Oqtane.Client/Services/ThemeService.cs index e678841c..c8f4aaa4 100644 --- a/Oqtane.Client/Services/ThemeService.cs +++ b/Oqtane.Client/Services/ThemeService.cs @@ -20,6 +20,10 @@ namespace Oqtane.Services List themes = await GetJsonAsync>(ApiUrl); return themes.OrderBy(item => item.Name).ToList(); } + public async Task GetThemeAsync(int themeId, int siteId) + { + return await GetJsonAsync($"{ApiUrl}/{themeId}?siteid={siteId}"); + } public List GetThemeControls(List themes) { @@ -38,6 +42,11 @@ namespace Oqtane.Services .SelectMany(item => item.Containers).ToList(); } + public async Task UpdateThemeAsync(Theme theme) + { + await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme); + } + public async Task DeleteThemeAsync(string themeName) { await DeleteAsync($"{ApiUrl}/{themeName}"); diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 4c662eb3..63d1afe7 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -256,7 +256,7 @@ namespace Oqtane.Controllers } // remove module definition - _moduleDefinitions.DeleteModuleDefinition(id, siteid); + _moduleDefinitions.DeleteModuleDefinition(id); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduledefinition.ModuleDefinitionId, SyncEventActions.Delete); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Definition {ModuleDefinitionName} Deleted", moduledefinition.Name); } diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 7ddad124..76fc7047 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -12,6 +12,8 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using System.Text.Json; using System.Net; +using System.Reflection.Metadata; +using System; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -23,14 +25,20 @@ namespace Oqtane.Controllers private readonly IThemeRepository _themes; private readonly IInstallationManager _installationManager; private readonly IWebHostEnvironment _environment; + private readonly ITenantManager _tenantManager; + private readonly ISyncManager _syncManager; private readonly ILogManager _logger; + private readonly Alias _alias; - public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ILogManager logger) + public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) { _themes = themes; _installationManager = installationManager; _environment = environment; + _tenantManager = tenantManager; + _syncManager = syncManager; _logger = logger; + _alias = tenantManager.GetAlias(); } // GET: api/ @@ -41,6 +49,41 @@ namespace Oqtane.Controllers return _themes.GetThemes(); } + // GET api//5?siteid=x + [HttpGet("{id}")] + public Theme Get(int id, string siteid) + { + int SiteId; + if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + { + return _themes.GetTheme(id, SiteId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {ThemeId} {SiteId}", id, siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public void Put(int id, [FromBody] Theme theme) + { + if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null) + { + _themes.UpdateTheme(theme); + _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Put Attempt {Theme}", theme); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + // DELETE api//xxx [HttpDelete("{themename}")] [Authorize(Roles = RoleNames.Host)] @@ -74,7 +117,7 @@ namespace Oqtane.Controllers } // remove theme - _themes.DeleteTheme(theme.ThemeName); + //_themes.DeleteTheme(theme.ThemeName); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Removed For {ThemeName}", theme.ThemeName); } else diff --git a/Oqtane.Server/Migrations/EntityBuilders/ThemeEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/ThemeEntityBuilder.cs new file mode 100644 index 00000000..1952e93c --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/ThemeEntityBuilder.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class ThemeEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "Theme"; + private readonly PrimaryKey _primaryKey = new("PK_Theme", x => x.ThemeId); + + public ThemeEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + } + + protected override ThemeEntityBuilder BuildTable(ColumnsBuilder table) + { + ThemeId = AddAutoIncrementColumn(table, "ThemeId"); + ThemeName = AddStringColumn(table, "ThemeName", 200); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder ThemeId { get; private set; } + + public OperationBuilder ThemeName { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs b/Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs new file mode 100644 index 00000000..1a0aac2a --- /dev/null +++ b/Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Master +{ + [DbContext(typeof(MasterDBContext))] + [Migration("Master.04.00.00.01")] + public class AddThemeTable : MultiDatabaseMigration + { + public AddThemeTable(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase); + themeEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Repository/Context/MasterDBContext.cs b/Oqtane.Server/Repository/Context/MasterDBContext.cs index a50c8fe2..4ade9f28 100644 --- a/Oqtane.Server/Repository/Context/MasterDBContext.cs +++ b/Oqtane.Server/Repository/Context/MasterDBContext.cs @@ -68,6 +68,7 @@ namespace Oqtane.Repository public virtual DbSet Job { get; set; } public virtual DbSet JobLog { get; set; } public virtual DbSet Setting { get; set; } + public virtual DbSet Theme { get; set; } public override int SaveChanges() { diff --git a/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs b/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs index b18ec993..eb4f45d9 100644 --- a/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs @@ -9,7 +9,7 @@ namespace Oqtane.Repository IEnumerable GetModuleDefinitions(int siteId); ModuleDefinition GetModuleDefinition(int moduleDefinitionId, int siteId); void UpdateModuleDefinition(ModuleDefinition moduleDefinition); - void DeleteModuleDefinition(int moduleDefinitionId, int siteId); + void DeleteModuleDefinition(int moduleDefinitionId); ModuleDefinition FilterModuleDefinition(ModuleDefinition moduleDefinition); } } diff --git a/Oqtane.Server/Repository/Interfaces/IThemeRepository.cs b/Oqtane.Server/Repository/Interfaces/IThemeRepository.cs index a7a81eee..b8d98aee 100644 --- a/Oqtane.Server/Repository/Interfaces/IThemeRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IThemeRepository.cs @@ -6,7 +6,9 @@ namespace Oqtane.Repository public interface IThemeRepository { IEnumerable GetThemes(); + Theme GetTheme(int themeId, int siteId); + void UpdateTheme(Theme theme); + void DeleteTheme(int themeId); List FilterThemes(List themes); - void DeleteTheme(string ThemeName); } } diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 5903a7ab..1d0c45f8 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -68,7 +68,7 @@ namespace Oqtane.Repository _cache.Remove($"moduledefinitions:{_tenants.GetAlias().SiteKey}"); } - public void DeleteModuleDefinition(int moduleDefinitionId,int siteId) + public void DeleteModuleDefinition(int moduleDefinitionId) { ModuleDefinition moduleDefinition = _db.ModuleDefinition.Find(moduleDefinitionId); _settings.DeleteSettings(EntityNames.ModuleDefinition, moduleDefinitionId); @@ -126,48 +126,48 @@ namespace Oqtane.Repository private List ProcessModuleDefinitions(int siteId) { // get module assemblies - List moduleDefinitions = LoadModuleDefinitionsFromAssemblies(); + List ModuleDefinitions = LoadModuleDefinitionsFromAssemblies(); // get module definitions in database - List moduledefs = _db.ModuleDefinition.ToList(); + List moduledefinitions = _db.ModuleDefinition.ToList(); // sync module assemblies with database - foreach (ModuleDefinition moduledefinition in moduleDefinitions) + foreach (ModuleDefinition ModuleDefinition in ModuleDefinitions) { - ModuleDefinition moduledef = moduledefs.Where(item => item.ModuleDefinitionName == moduledefinition.ModuleDefinitionName).FirstOrDefault(); - if (moduledef == null) + ModuleDefinition moduledefinition = moduledefinitions.Where(item => item.ModuleDefinitionName == ModuleDefinition.ModuleDefinitionName).FirstOrDefault(); + if (moduledefinition == null) { // new module definition - moduledef = new ModuleDefinition { ModuleDefinitionName = moduledefinition.ModuleDefinitionName }; - _db.ModuleDefinition.Add(moduledef); + moduledefinition = new ModuleDefinition { ModuleDefinitionName = ModuleDefinition.ModuleDefinitionName }; + _db.ModuleDefinition.Add(moduledefinition); _db.SaveChanges(); - moduledefinition.Version = ""; + ModuleDefinition.Version = ""; } else { // override user customizable property values - moduledefinition.Name = (!string.IsNullOrEmpty(moduledef.Name)) ? moduledef.Name : moduledefinition.Name; - moduledefinition.Description = (!string.IsNullOrEmpty(moduledef.Description)) ? moduledef.Description : moduledefinition.Description; - moduledefinition.Categories = (!string.IsNullOrEmpty(moduledef.Categories)) ? moduledef.Categories : moduledefinition.Categories; + ModuleDefinition.Name = (!string.IsNullOrEmpty(moduledefinition.Name)) ? moduledefinition.Name : ModuleDefinition.Name; + ModuleDefinition.Description = (!string.IsNullOrEmpty(moduledefinition.Description)) ? moduledefinition.Description : ModuleDefinition.Description; + ModuleDefinition.Categories = (!string.IsNullOrEmpty(moduledefinition.Categories)) ? moduledefinition.Categories : ModuleDefinition.Categories; // manage releaseversions in cases where it was not provided or is lower than the module version - if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions) || Version.Parse(moduledefinition.Version).CompareTo(Version.Parse(moduledefinition.ReleaseVersions.Split(',').Last())) > 0) + if (string.IsNullOrEmpty(ModuleDefinition.ReleaseVersions) || Version.Parse(ModuleDefinition.Version).CompareTo(Version.Parse(ModuleDefinition.ReleaseVersions.Split(',').Last())) > 0) { - moduledefinition.ReleaseVersions = moduledefinition.Version; + ModuleDefinition.ReleaseVersions = ModuleDefinition.Version; } - moduledefinition.Version = moduledef.Version; + ModuleDefinition.Version = moduledefinition.Version; // remove module definition from list as it is already synced - moduledefs.Remove(moduledef); + moduledefinitions.Remove(moduledefinition); } - moduledefinition.ModuleDefinitionId = moduledef.ModuleDefinitionId; - moduledefinition.CreatedBy = moduledef.CreatedBy; - moduledefinition.CreatedOn = moduledef.CreatedOn; - moduledefinition.ModifiedBy = moduledef.ModifiedBy; - moduledefinition.ModifiedOn = moduledef.ModifiedOn; + ModuleDefinition.ModuleDefinitionId = moduledefinition.ModuleDefinitionId; + ModuleDefinition.CreatedBy = moduledefinition.CreatedBy; + ModuleDefinition.CreatedOn = moduledefinition.CreatedOn; + ModuleDefinition.ModifiedBy = moduledefinition.ModifiedBy; + ModuleDefinition.ModifiedOn = moduledefinition.ModifiedOn; } // any remaining module definitions are orphans - foreach (ModuleDefinition moduledefinition in moduledefs) + foreach (ModuleDefinition moduledefinition in moduledefinitions) { _db.ModuleDefinition.Remove(moduledefinition); // delete _db.SaveChanges(); @@ -181,8 +181,8 @@ namespace Oqtane.Repository // get settings for site var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList(); - // populate module definition permissions - foreach (ModuleDefinition moduledefinition in moduleDefinitions) + // populate module definition site settings and permissions + foreach (ModuleDefinition moduledefinition in ModuleDefinitions) { moduledefinition.SiteId = siteId; @@ -218,7 +218,7 @@ namespace Oqtane.Repository } // clean up any orphaned permissions - var ids = new HashSet(moduleDefinitions.Select(item => item.ModuleDefinitionId)); + var ids = new HashSet(ModuleDefinitions.Select(item => item.ModuleDefinitionId)); foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId))) { try @@ -232,7 +232,7 @@ namespace Oqtane.Repository } } - return moduleDefinitions; + return ModuleDefinitions; } private List LoadModuleDefinitionsFromAssemblies() diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 4ca2c40e..413ef6bb 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -4,39 +4,168 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.EntityFrameworkCore; +using System.Security; using Microsoft.Extensions.Caching.Memory; +using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Shared; using Oqtane.Themes; +using System.Reflection.Metadata; namespace Oqtane.Repository { public class ThemeRepository : IThemeRepository { + private MasterDBContext _db; private readonly IMemoryCache _cache; + private readonly ITenantManager _tenants; + private readonly ISettingRepository _settings; + private readonly string settingprefix = "SiteEnabled:"; - public ThemeRepository(IMemoryCache cache) + public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings) { + _db = context; _cache = cache; + _tenants = tenants; + _settings = settings; } public IEnumerable GetThemes() { - return LoadThemes(); + // for consistency siteid should be passed in as parameter, but this would require breaking change + return LoadThemes(_tenants.GetAlias().SiteId); } - private List LoadThemes() + public Theme GetTheme(int themeId, int siteId) { - // get module definitions - List themes = _cache.GetOrCreate("themes", entry => + List themes = LoadThemes(siteId); + return themes.Find(item => item.ThemeId == themeId); + } + + public void UpdateTheme(Theme theme) + { + _db.Entry(theme).State = EntityState.Modified; + _db.SaveChanges(); + + var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}"; + var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname); + if (setting == null) + { + _settings.AddSetting(new Setting { EntityName = EntityNames.Theme, EntityId = theme.ThemeId, SettingName = settingname, SettingValue = theme.IsEnabled.ToString(), IsPrivate = true }); + } + else + { + setting.SettingValue = theme.IsEnabled.ToString(); + _settings.UpdateSetting(setting); + } + + _cache.Remove($"themes:{_tenants.GetAlias().SiteKey}"); + } + + public void DeleteTheme(int themeId) + { + Theme theme = _db.Theme.Find(themeId); + _settings.DeleteSettings(EntityNames.Theme, themeId); + _db.Theme.Remove(theme); + _db.SaveChanges(); + _cache.Remove($"themes:{_tenants.GetAlias().SiteKey}"); + } + + public List FilterThemes(List themes) + { + var Themes = new List(); + + foreach (Theme theme in themes.Where(item => item.IsEnabled)) + { + var Theme = new Theme(); + Theme.ThemeName = theme.ThemeName; + Theme.Name = theme.Name; + Theme.Resources = theme.Resources; + Theme.Themes = theme.Themes; + Theme.Containers = theme.Containers; + Themes.Add(Theme); + } + + return Themes; + } + + private List LoadThemes(int siteId) + { + // get themes + List themes = _cache.GetOrCreate($"themes:{_tenants.GetAlias().SiteKey}", entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(30); - return LoadThemesFromAssemblies(); + return ProcessThemes(siteId); }); return themes; } + private List ProcessThemes(int siteId) + { + // get themes + List Themes = LoadThemesFromAssemblies(); + + // get themes in database + List themes = _db.Theme.ToList(); + + // sync theme assemblies with database + foreach (Theme Theme in Themes) + { + Theme theme = themes.Where(item => item.ThemeName == Theme.ThemeName).FirstOrDefault(); + if (theme == null) + { + // new theme + theme = new Theme { ThemeName = Theme.ThemeName }; + _db.Theme.Add(theme); + _db.SaveChanges(); + } + else + { + // remove theme from list as it is already synced + themes.Remove(theme); + } + + Theme.ThemeId = theme.ThemeId; + Theme.CreatedBy = theme.CreatedBy; + Theme.CreatedOn = theme.CreatedOn; + Theme.ModifiedBy = theme.ModifiedBy; + Theme.ModifiedOn = theme.ModifiedOn; + } + + // any remaining themes are orphans + foreach (Theme theme in themes) + { + _db.Theme.Remove(theme); // delete + _db.SaveChanges(); + } + + if (siteId != -1) + { + // get settings for site + var settings = _settings.GetSettings(EntityNames.Theme).ToList(); + + // populate theme site settings + foreach (Theme theme in Themes) + { + theme.SiteId = siteId; + + var setting = settings.FirstOrDefault(item => item.EntityId == theme.ThemeId && item.SettingName == $"{settingprefix}{_tenants.GetAlias().SiteKey}"); + if (setting != null) + { + theme.IsEnabled = bool.Parse(setting.SettingValue); + } + else + { + theme.IsEnabled = theme.IsAutoEnabled; + } + } + } + + return Themes; + } + private List LoadThemesFromAssemblies() { List themes = new List(); @@ -143,28 +272,5 @@ namespace Oqtane.Repository } return themes; } - - public List FilterThemes(List themes) - { - var Themes = new List(); - - foreach (Theme theme in themes) - { - var Theme = new Theme(); - Theme.ThemeName = theme.ThemeName; - Theme.Name = theme.Name; - Theme.Resources = theme.Resources; - Theme.Themes = theme.Themes; - Theme.Containers = theme.Containers; - Themes.Add(Theme); - } - - return Themes; - } - - public void DeleteTheme(string ThemeName) - { - _cache.Remove("themes"); - } } } diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index 1ff07b34..b795e45f 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -7,7 +7,7 @@ namespace Oqtane.Models /// /// Information about a Theme in Oqtane. /// - public class Theme + public class Theme : ModelBase { public Theme() { @@ -25,68 +25,81 @@ namespace Oqtane.Models Resources = null; } + /// + /// Reference to the . + /// + public int ThemeId { get; set; } + /// /// Full Namespace / Identifier of the Theme. /// public string ThemeName { get; set; } - /// - /// Nice Name of the Theme. - /// + // additional ITheme properties + [NotMapped] public string Name { get; set; } - /// - /// Version as determined by the DLL / NuGet Package. - /// + [NotMapped] public string Version { get; set; } - /// - /// Author / Creator of the Theme. - /// + [NotMapped] public string Owner { get; set; } - /// - /// URL (in NuGet) of the Theme - /// + [NotMapped] public string Url { get; set; } - /// - /// Author Contact information - /// + [NotMapped] public string Contact { get; set; } - /// - /// Theme License, like `MIT` etc. - /// + [NotMapped] public string License { get; set; } - /// - /// Theme Dependencies (DLLs) which the system will check if they exist - /// + [NotMapped] public string Dependencies { get; set; } - + [NotMapped] public string ThemeSettingsType { get; set; } // added in 2.0.2 + + [NotMapped] public string ContainerSettingsType { get; set; } // added in 2.0.2 + + [NotMapped] public string PackageName { get; set; } // added in 2.1.0 + + [NotMapped] public List Resources { get; set; } // added in 4.0.0 + [NotMapped] + public bool IsAutoEnabled { get; set; } = true; // added in 4.0.0 + // internal properties + [NotMapped] + public int SiteId { get; set; } + [NotMapped] + public bool IsEnabled { get; set; } + [NotMapped] public string AssemblyName { get; set; } + [NotMapped] public List Themes { get; set; } + [NotMapped] public List Containers { get; set; } + [NotMapped] public string Template { get; set; } #region Obsolete Properties [Obsolete("This property is obsolete. Use Themes instead.", false)] + [NotMapped] public string ThemeControls { get; set; } [Obsolete("This property is obsolete. Use Layouts instead.", false)] + [NotMapped] public string PaneLayouts { get; set; } [Obsolete("This property is obsolete. Use Containers instead.", false)] + [NotMapped] public string ContainerControls { get; set; } [Obsolete("This property is obsolete.", false)] + [NotMapped] public List Layouts { get; set; } #endregion diff --git a/Oqtane.Shared/Shared/EntityNames.cs b/Oqtane.Shared/Shared/EntityNames.cs index 1e480c3b..1f9e162d 100644 --- a/Oqtane.Shared/Shared/EntityNames.cs +++ b/Oqtane.Shared/Shared/EntityNames.cs @@ -17,6 +17,7 @@ namespace Oqtane.Shared public const string Setting = "Setting"; public const string Site = "Site"; public const string Tenant = "Tenant"; + public const string Theme = "Theme"; public const string UrlMapping = "UrlMapping"; public const string User = "User"; public const string UserRole = "UserRole";