diff --git a/Oqtane.Application/.template.config/template.json b/Oqtane.Application/.template.config/template.json index 9b292029..ebcd1b03 100644 --- a/Oqtane.Application/.template.config/template.json +++ b/Oqtane.Application/.template.config/template.json @@ -7,13 +7,80 @@ "Blazor", "Oqtane" ], + "name": "Oqtane Application Template", + "shortName": "oqtane-app", + "defaultName": "MyCompany.MyProject", + "identity": "Oqtane.Application.Template", "tags": { "language": "C#", - "type": "project" + "type": "solution", + "editorTreatAs":"solution" }, - "identity": "Oqtane.Application.Template", - "name": "Oqtane Application Template For Blazor", - "shortName": "oqtane-app", "sourceName": "Oqtane.Application", - "preferNameDirectory": true + "preferNameDirectory": true, + "guids": [ + "04B05448-788F-433D-92C0-FED35122D45A", + "AA8E58A1-CD09-4208-BF66-A8BB341FD669", + "18D73F73-D7BE-4388-85BA-FBD9AC96FCA2" + ], + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project", + "datatype": "choice", + "choices": [ + { + "choice": "net9.0", + "description": "Target net9.0" + } + ], + "replaces": "net9.0", + "defaultValue": "net9.0" + }, + "HttpPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use for the HTTP endpoint in launchSettings.json." + }, + "HttpPortGenerated": { + "type": "generated", + "generator": "port" + }, + "HttpPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpPort", + "fallbackVariableName": "HttpPortGenerated" + }, + "replaces": "44358" + }, + "HttpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use for the HTTPS endpoint in launchSettings.json." + }, + "HttpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 44300, + "high": 44399 + } + }, + "HttpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpsPort", + "fallbackVariableName": "HttpsPortGenerated" + }, + "replaces": "44359" + } + }, + "primaryOutputs": [ + { + "path": "Oqtane.Application.sln" + } + ] } \ No newline at end of file diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index 6dc0740c..c7bd2e5c 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -1,23 +1,30 @@ - - net9.0 - Exe - 1.0.0 - Oqtane.Application.Client.Oqtane - true - Default - true - false - false - + + net9.0 + Exe + 1.0.0 + Oqtane.Application.Client.Oqtane + Default + true + false + false + - - - + + + + + + + - - - + + + + + + + diff --git a/Oqtane.Application/README.md b/Oqtane.Application/README.md index 62a93845..0a6927de 100644 --- a/Oqtane.Application/README.md +++ b/Oqtane.Application/README.md @@ -9,7 +9,7 @@ cd MyCompany.MyProject dotnet build cd Server dotnet run -browse to http://localhost:5001 +browse to Url ``` When using this approach you do not need to have a local copy of the oqtane.framework source code - you simply utilize Oqtane as a standard application dependency. diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 71dbe6a0..aaf443fa 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -1,23 +1,29 @@  - - net9.0 - 1.0.0 - Oqtane.Application.Server.Oqtane - true - true - none - false - false - + + net9.0 + 1.0.0 + Oqtane.Application.Server.Oqtane + true + none + false + false + - - - - + + + + + + - - - + + + + + + + + diff --git a/Oqtane.Application/Server/Properties/launchSettings.json b/Oqtane.Application/Server/Properties/launchSettings.json index dc8793f7..f56a17f7 100644 --- a/Oqtane.Application/Server/Properties/launchSettings.json +++ b/Oqtane.Application/Server/Properties/launchSettings.json @@ -6,7 +6,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5000", + "applicationUrl": "http://localhost:44358", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -16,7 +16,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "https://localhost:44359;http://localhost:44358", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj index 02dee1e3..1125b6c0 100644 --- a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj +++ b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj @@ -1,14 +1,17 @@ - - net9.0 - 1.0.0 - Oqtane.Application.Shared.Oqtane - true - + + net9.0 + 1.0.0 + Oqtane.Application.Shared.Oqtane + - - - + + + + + + + diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index ef8bbc95..e9caa80f 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -14,7 +14,7 @@ @if (_initialized) { - +
@@ -236,11 +236,10 @@ private DateTime _createdon; private string _modifiedby; private DateTime _modifiedon; - private List _pagesWithModules; -#pragma warning disable 649 private PermissionGrid _permissionGrid; -#pragma warning restore 649 + + private List _pagesWithModules; private List _packages; private List _languages; diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index a4bc6500..36385232 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -269,8 +269,16 @@ if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList))) { _themetype = PageState.Site.DefaultThemeType; - _themes = ThemeService.GetThemeControls(PageState.Site.Themes); - _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); + var themes = new List(); + foreach (var theme in PageState.Site.Themes) + { + if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList)) + { + themes.Add(theme); + } + } + _themes = ThemeService.GetThemeControls(themes); + _containers = ThemeService.GetContainerControls(themes, _themetype); _containertype = PageState.Site.DefaultContainerType; _children = new List(); foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index df9db9bb..2811bd87 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -443,8 +443,16 @@ { _themetype = PageState.Site.DefaultThemeType; } - _themes = ThemeService.GetThemeControls(PageState.Site.Themes); - _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); + var themes = new List(); + foreach (var theme in PageState.Site.Themes) + { + if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList)) + { + themes.Add(theme); + } + } + _themes = ThemeService.GetThemeControls(themes); + _containers = ThemeService.GetContainerControls(themes, _themetype); _containertype = _page.DefaultContainerType; if (string.IsNullOrEmpty(_containertype)) { diff --git a/Oqtane.Client/Modules/Admin/Profiles/Edit.razor b/Oqtane.Client/Modules/Admin/Profiles/Edit.razor index e2e16b55..467f0d82 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Edit.razor @@ -2,6 +2,7 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IProfileService ProfileService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -56,9 +57,25 @@
- +
- +
+ @if (_optiontype == "Settings") + { + + } + else + { + + } + +
@@ -95,7 +112,7 @@
@SharedLocalizer["Cancel"] - @if (PageState.QueryString.ContainsKey("id")) + @if (PageState.QueryString.ContainsKey("id")) {

@@ -116,6 +133,8 @@ private string _rows = "1"; private string _defaultvalue = string.Empty; private string _options = string.Empty; + private string _optiontype = "Settings"; + private List _entitynames; private string _validation = string.Empty; private string _autocomplete = string.Empty; private string _isrequired = "False"; @@ -133,6 +152,8 @@ { try { + _entitynames = await SettingService.GetEntityNamesAsync(); + if (PageState.QueryString.ContainsKey("id")) { _profileid = Int32.Parse(PageState.QueryString["id"]); @@ -148,6 +169,11 @@ _rows = profile.Rows.ToString(); _defaultvalue = profile.DefaultValue; _options = profile.Options; + if (_options.StartsWith("EntityName:")) + { + _optiontype = "Options"; + _options = _options.Substring(11); + } _validation = profile.Validation; _autocomplete = profile.Autocomplete; _isrequired = profile.IsRequired.ToString(); @@ -166,6 +192,18 @@ } } + private void ToggleOptionType() + { + if (_optiontype == "Options") + { + _optiontype = "Settings"; + } + else + { + _optiontype = "Options"; + } + } + private async Task SaveProfile() { validated = true; @@ -193,7 +231,14 @@ profile.MaxLength = int.Parse(_maxlength); profile.Rows = int.Parse(_rows); profile.DefaultValue = _defaultvalue; - profile.Options = _options; + if (_optiontype == "Options" && !string.IsNullOrEmpty(_options)) + { + profile.Options = "EntityName:" + _options; + } + else + { + profile.Options = _options; + } profile.Validation = _validation; profile.Autocomplete = _autocomplete; profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired)); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index b921ab38..525f45df 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -592,9 +592,17 @@ { _faviconfileid = site.FaviconFileId.Value; } - _themes = ThemeService.GetThemeControls(PageState.Site.Themes); + var themes = new List(); + foreach (var theme in PageState.Site.Themes) + { + if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList)) + { + themes.Add(theme); + } + } + _themes = ThemeService.GetThemeControls(themes); _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; - _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); + _containers = ThemeService.GetContainerControls(themes, _themetype); _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty); diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 96949077..383a839e 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -216,7 +216,7 @@ else _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); } _urls = PageState.Alias.Name; - _themeList = await ThemeService.GetThemesAsync(); + _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId); _themes = ThemeService.GetThemeControls(_themeList); if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) { diff --git a/Oqtane.Client/Modules/Admin/Themes/Add.razor b/Oqtane.Client/Modules/Admin/Themes/Add.razor index cece5ed4..0da378fa 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Add.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Add.razor @@ -195,7 +195,7 @@ { try { - _themes = await ThemeService.GetThemesAsync(); + _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId); await LoadPackages(); _initialized = true; } diff --git a/Oqtane.Client/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor index 125e2846..4e7560e5 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -9,84 +9,98 @@ @if (_initialized) { - -
-
- -
- + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ @if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~")) + { + @Localizer["View License"] + } + else + { + + } +
+
+
+
+
+ + @SharedLocalizer["Cancel"] +
+
+ +
+ +
+
+
-
- -
- -
-
-
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- @if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~")) - { - @Localizer["View License"] - } - else - { - - } -
-
-
-
-
- - @SharedLocalizer["Cancel"] -
-
- +
+ + @SharedLocalizer["Cancel"] + + } @code { @@ -103,11 +117,14 @@ private string _url = ""; private string _contact = ""; private string _license = ""; + private List _permissions = null; private string _createdby; private DateTime _createdon; private string _modifiedby; private DateTime _modifiedon; + private PermissionGrid _permissionGrid; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; protected override async Task OnInitializedAsync() @@ -126,6 +143,7 @@ _url = theme.Url; _contact = theme.Contact; _license = theme.License; + _permissions = theme.PermissionList; _createdby = theme.CreatedBy; _createdon = theme.CreatedOn; _modifiedby = theme.ModifiedBy; @@ -152,6 +170,7 @@ var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); theme.Name = _name; theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); + theme.PermissionList = _permissionGrid.GetPermissionList(); await ThemeService.UpdateThemeAsync(theme); await logger.LogInformation("Theme Saved {Theme}", theme); NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Client/Modules/Admin/Themes/Index.razor b/Oqtane.Client/Modules/Admin/Themes/Index.razor index 719802bb..f3f081e0 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Index.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Index.razor @@ -78,7 +78,7 @@ else { try { - _themes = await ThemeService.GetThemesAsync(); + _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId); _packages = await PackageService.GetPackageUpdatesAsync("theme"); } catch (Exception ex) @@ -161,7 +161,7 @@ else { try { - await ThemeService.DeleteThemeAsync(Theme.ThemeName); + await ThemeService.DeleteThemeAsync(Theme.ThemeId, PageState.Site.SiteId); AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); } diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index c3ac1846..4bd49824 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -345,11 +345,11 @@ try { FolderId = int.Parse((string)e.Value); - await OnSelectFolder.InvokeAsync(FolderId); FileId = -1; GetFolderPermission(); await SetImage(); await GetFiles(); + await OnSelectFolder.InvokeAsync(FolderId); StateHasChanged(); } catch (Exception ex) @@ -364,11 +364,11 @@ { _message = string.Empty; FileId = int.Parse((string)e.Value); + await SetImage(); #pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); #pragma warning restore CS0618 await OnSelectFile.InvokeAsync(FileId); - await SetImage(); StateHasChanged(); } @@ -460,13 +460,14 @@ } } + await SetImage(); + await OnUpload.InvokeAsync(FileId); #pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); #pragma warning restore CS0618 await OnSelectFile.InvokeAsync(FileId); - await SetImage(); await GetFiles(); StateHasChanged(); } @@ -518,12 +519,13 @@ } FileId = -1; + await SetImage(); + #pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); #pragma warning restore CS0618 await OnSelectFile.InvokeAsync(FileId); - await SetImage(); await GetFiles(); StateHasChanged(); } diff --git a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs new file mode 100644 index 00000000..b8623e51 --- /dev/null +++ b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs @@ -0,0 +1,12 @@ +// This is just a placeholder file +// It is necessary for the documentation to successfully build this project. +// Reason is that docfx will run the .net compiler and find references +// to this class in the project. +// But since the real class is just a .razor file, ATM docfx will fail. +// +// Note added 2025-09-23 by @tvatavuk. +// We hope that as .net and docfx improve, the razor-compiler will work in that scenario +// as well, and this file can be removed. + +namespace Oqtane.Modules.Controls; +public partial class RadzenTextEditor; diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index b5a3d38a..01007708 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -26,7 +26,7 @@ - + diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index 4421b90a..eb32861b 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -183,8 +183,8 @@ Runtimes: - - Definition + + Module Information diff --git a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx index d67a4f7a..4810d519 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx @@ -157,7 +157,7 @@ The default value for this profile item - A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from custom Settings (ie. 'EntityName:Countries'). + A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from Settings. Should a user be required to provide a value for this profile item? @@ -201,4 +201,10 @@ Autocomplete: + + Options + + + Settings + diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index 69f9bdd8..179d4132 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -180,4 +180,10 @@ View License + + Themex + + + Permissionsx + \ No newline at end of file diff --git a/Oqtane.Client/Services/FileService.cs b/Oqtane.Client/Services/FileService.cs index 2b45455a..75256fc8 100644 --- a/Oqtane.Client/Services/FileService.cs +++ b/Oqtane.Client/Services/FileService.cs @@ -101,7 +101,6 @@ namespace Oqtane.Services /// Unzips the contents of a zip file /// /// Reference to the - /// /// Task UnzipFileAsync(int fileId); } diff --git a/Oqtane.Client/Services/ThemeService.cs b/Oqtane.Client/Services/ThemeService.cs index 4fd1d9eb..942ee7e9 100644 --- a/Oqtane.Client/Services/ThemeService.cs +++ b/Oqtane.Client/Services/ThemeService.cs @@ -17,8 +17,9 @@ namespace Oqtane.Services /// /// Returns a list of available themes /// + /// /// - Task> GetThemesAsync(); + Task> GetThemesAsync(int siteId); /// /// Returns a specific theme @@ -69,9 +70,10 @@ namespace Oqtane.Services /// /// Deletes a theme /// - /// + /// + /// /// - Task DeleteThemeAsync(string themeName); + Task DeleteThemeAsync(int themeId, int siteId); /// /// Creates a new theme @@ -103,9 +105,9 @@ namespace Oqtane.Services private string ApiUrl => CreateApiUrl("Theme"); - public async Task> GetThemesAsync() + public async Task> GetThemesAsync(int siteId) { - List themes = await GetJsonAsync>(ApiUrl); + List themes = await GetJsonAsync>($"{ApiUrl}?siteid={siteId}"); return themes.OrderBy(item => item.Name).ToList(); } public async Task GetThemeAsync(int themeId, int siteId) @@ -139,9 +141,9 @@ namespace Oqtane.Services await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme); } - public async Task DeleteThemeAsync(string themeName) + public async Task DeleteThemeAsync(int themeId, int siteId) { - await DeleteAsync($"{ApiUrl}/{themeName}"); + await DeleteAsync($"{ApiUrl}/{themeId}?siteid={siteId}"); } public async Task CreateThemeAsync(Theme theme) diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 22294ad2..93b6ca3a 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -23,7 +23,7 @@ - + diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index e018a121..4e18cc4d 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -27,7 +27,7 @@ - + diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 6446af85..486af9fc 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -252,7 +252,7 @@ namespace Oqtane.Controllers } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId} {SiteId}", id, siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 207b8a80..041c70cf 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -308,7 +308,7 @@ namespace Oqtane.Controllers // GET: api//entitynames [HttpGet("entitynames")] - [Authorize(Roles = RoleNames.Host)] + [Authorize(Roles = RoleNames.Admin)] public IEnumerable GetEntityNames() { return _settings.GetEntityNames(); @@ -316,7 +316,7 @@ namespace Oqtane.Controllers // GET: api//entityids?entityname=x [HttpGet("entityids")] - [Authorize(Roles = RoleNames.Host)] + [Authorize(Roles = RoleNames.Admin)] public IEnumerable GetEntityIds(string entityName) { return _settings.GetEntityIds(entityName); diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index cf1c24fc..eb250069 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -14,6 +14,9 @@ using System.Text.Json; using System.Net; using System; using Microsoft.Extensions.DependencyInjection; +using System.Reflection.Metadata; +using Oqtane.Security; +using System.Security.Policy; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -26,30 +29,50 @@ namespace Oqtane.Controllers private readonly IInstallationManager _installationManager; private readonly IWebHostEnvironment _environment; private readonly ITenantManager _tenantManager; + private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; private readonly IServiceProvider _serviceProvider; - public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider) + public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider) { _themes = themes; _installationManager = installationManager; _environment = environment; _tenantManager = tenantManager; + _userPermissions = userPermissions; _syncManager = syncManager; _logger = logger; _alias = tenantManager.GetAlias(); _serviceProvider = serviceProvider; } - // GET: api/ + // GET: api/?siteid=x [HttpGet] [Authorize(Roles = RoleNames.Registered)] - public IEnumerable Get() + public IEnumerable Get(string siteid) { - return _themes.GetThemes(); - } + int SiteId; + if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + { + List themes = new List(); + foreach (Theme theme in _themes.GetThemes(SiteId)) + { + if (_userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList)) + { + themes.Add(theme); + } + } + return themes; + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {SiteId}", siteid); + HttpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden; + return null; + } +} // GET api//5?siteid=x [HttpGet("{id}")] @@ -58,7 +81,24 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - return _themes.GetTheme(id, SiteId); + Theme theme = _themes.GetTheme(id, SiteId); + if (theme != null && _userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList)) + { + return theme; + } + else + { + if (theme != null) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {ThemeId} {SiteId}", id, siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + return null; + } } else { @@ -86,14 +126,13 @@ namespace Oqtane.Controllers } } - // DELETE api//xxx + // DELETE api//5?siteid=x [HttpDelete("{themename}")] [Authorize(Roles = RoleNames.Host)] - public void Delete(string themename) + public void Delete(int id, int siteid) { - List themes = _themes.GetThemes().ToList(); - Theme theme = themes.Where(item => item.ThemeName == themename).FirstOrDefault(); - if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId) + Theme theme = _themes.GetTheme(id, siteid); + if (theme != null && theme.SiteId == _alias.SiteId && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId) { // remove theme assets if (_installationManager.UninstallPackage(theme.PackageName)) @@ -126,7 +165,7 @@ namespace Oqtane.Controllers } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {Themename}", themename); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {ThemeId} {SiteId}", id, siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index c85663e8..7771d8c1 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -179,38 +179,22 @@ namespace Oqtane.Infrastructure fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", ""); fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName; } - try + if (MailboxAddress.TryParse(fromEmail, out from)) { - // exception handler is necessary because of https://github.com/jstedfast/MimeKit/issues/1186 - if (MailboxAddress.TryParse(fromEmail, out _)) - { - from = new MailboxAddress(fromName, fromEmail); - } + from.Name = fromName; } - catch - { - // parse error creating sender mailbox address - } - if (from == null) + else { mailboxAddressValidationError += $" Invalid Sender: {fromName} <{fromEmail}>"; } // recipient - try + if (MailboxAddress.TryParse(toEmail, out to)) { - // exception handler is necessary because of https://github.com/jstedfast/MimeKit/issues/1186 - if (MailboxAddress.TryParse(toEmail, out _)) - { - to = new MailboxAddress(toName, toEmail); - } + to.Name = toName; } - catch - { - // parse error creating recipient mailbox address - } - if (to == null) + else { mailboxAddressValidationError += $" Invalid Recipient: {toName} <{toEmail}>"; } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 2f235cab..6edfd77f 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -48,7 +48,7 @@ - + diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 10226d28..d8bb7f4e 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -386,6 +386,7 @@ namespace Oqtane.Repository moduledefinition.Categories = "Common"; } + // default permissions if (moduledefinition.Categories == "Admin") { var shortName = moduledefinition.ModuleDefinitionName.Replace("Oqtane.Modules.Admin.", "").Replace(", Oqtane.Client", ""); @@ -455,18 +456,21 @@ namespace Oqtane.Repository private List ClonePermissions(int siteId, List permissionList) { var permissions = new List(); - foreach (var p in permissionList) + if (permissionList != null) { - var permission = new Permission(); - permission.SiteId = siteId; - permission.EntityName = p.EntityName; - permission.EntityId = p.EntityId; - permission.PermissionName = p.PermissionName; - permission.RoleId = null; - permission.RoleName = p.RoleName; - permission.UserId = p.UserId; - permission.IsAuthorized = p.IsAuthorized; - permissions.Add(permission); + foreach (var p in permissionList) + { + var permission = new Permission(); + permission.SiteId = siteId; + permission.EntityName = p.EntityName; + permission.EntityId = p.EntityId; + permission.PermissionName = p.PermissionName; + permission.RoleId = null; + permission.RoleName = p.RoleName; + permission.UserId = p.UserId; + permission.IsAuthorized = p.IsAuthorized; + permissions.Add(permission); + } } return permissions; } diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index f791aeac..bd84354e 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -135,7 +135,7 @@ namespace Oqtane.Repository if (site != null) { // initialize theme Assemblies - site.Themes = _themeRepository.GetThemes().ToList(); + site.Themes = _themeRepository.GetThemes(site.SiteId).ToList(); // initialize module Assemblies var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId); diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 56331a37..ba901dd9 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -15,7 +15,7 @@ namespace Oqtane.Repository { public interface IThemeRepository { - IEnumerable GetThemes(); + IEnumerable GetThemes(int siteId); Theme GetTheme(int themeId, int siteId); void UpdateTheme(Theme theme); void DeleteTheme(int themeId); @@ -26,24 +26,25 @@ namespace Oqtane.Repository { private MasterDBContext _db; private readonly IMemoryCache _cache; + private readonly IPermissionRepository _permissions; private readonly ITenantManager _tenants; private readonly ISettingRepository _settings; private readonly IServerStateManager _serverState; private readonly string settingprefix = "SiteEnabled:"; - public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState) + public ThemeRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState) { _db = context; _cache = cache; + _permissions = permissions; _tenants = tenants; _settings = settings; _serverState = serverState; } - public IEnumerable GetThemes() + public IEnumerable GetThemes(int siteId) { - // for consistency siteid should be passed in as parameter, but this would require breaking change - return LoadThemes(_tenants.GetAlias().SiteId); + return LoadThemes(siteId); } public Theme GetTheme(int themeId, int siteId) @@ -56,6 +57,7 @@ namespace Oqtane.Repository { _db.Entry(theme).State = EntityState.Modified; _db.SaveChanges(); + _permissions.UpdatePermissions(theme.SiteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList); var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}"; var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname); @@ -96,6 +98,7 @@ namespace Oqtane.Repository Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.PackageName = theme.PackageName; + Theme.PermissionList = theme.PermissionList; Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); Themes.Add(Theme); } @@ -176,6 +179,9 @@ namespace Oqtane.Repository var siteKey = _tenants.GetAlias().SiteKey; var assemblies = new List(); + // get all module definition permissions for site + List permissions = _permissions.GetPermissions(siteId, EntityNames.Theme).ToList(); + // get settings for site var settings = _settings.GetSettings(EntityNames.Theme).ToList(); @@ -212,6 +218,26 @@ namespace Oqtane.Repository } } } + + if (permissions.Count == 0) + { + // no module definition permissions exist for this site + theme.PermissionList = ClonePermissions(siteId, theme.PermissionList); + _permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList); + } + else + { + if (permissions.Any(item => item.EntityId == theme.ThemeId)) + { + theme.PermissionList = permissions.Where(item => item.EntityId == theme.ThemeId).ToList(); + } + else + { + // permissions for theme do not exist for this site + theme.PermissionList = ClonePermissions(siteId, theme.PermissionList); + _permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList); + } + } } // cache site assemblies @@ -220,6 +246,20 @@ namespace Oqtane.Repository { if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly); } + + // clean up any orphaned permissions + var ids = new HashSet(Themes.Select(item => item.ThemeId)); + foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId))) + { + try + { + _permissions.DeletePermission(permission.PermissionId); + } + catch + { + // multi-threading can cause a race condition to occur + } + } } return Themes; @@ -295,6 +335,14 @@ namespace Oqtane.Repository } } } + + // default permissions + theme.PermissionList = new List + { + new Permission(PermissionNames.Utilize, RoleNames.Admin, true), + new Permission(PermissionNames.Utilize, RoleNames.Registered, true) + }; + Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}"); themes.Add(theme); index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType); @@ -335,5 +383,27 @@ namespace Oqtane.Repository } return themes; } + + private List ClonePermissions(int siteId, List permissionList) + { + var permissions = new List(); + if (permissionList != null) + { + foreach (var p in permissionList) + { + var permission = new Permission(); + permission.SiteId = siteId; + permission.EntityName = p.EntityName; + permission.EntityId = p.EntityId; + permission.PermissionName = p.PermissionName; + permission.RoleId = null; + permission.RoleName = p.RoleName; + permission.UserId = p.UserId; + permission.IsAuthorized = p.IsAuthorized; + permissions.Add(permission); + } + } + return permissions; + } } } diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index f316b766..25a9ec92 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -144,7 +144,7 @@ namespace Oqtane.Services } // themes - site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); + site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList()); // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index a67a2d97..61b84595 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -94,6 +94,9 @@ namespace Oqtane.Models [NotMapped] public List Containers { get; set; } + [NotMapped] + public List PermissionList { get; set; } + [NotMapped] public string Template { get; set; }