From 3db2d03a37047010314ca808a17856b7d62ec7b6 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 20 Sep 2025 11:49:10 +0200 Subject: [PATCH 01/12] Package Updates Updated Client Radzen.Blazo to 7.3.5 Updated Server HtmlAgilityPack 1.12.3 --- Oqtane.Client/Oqtane.Client.csproj | 2 +- Oqtane.Server/Oqtane.Server.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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.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 @@ - + From 52745b194657ea960e683aef9e6f58e455d44f44 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 21 Sep 2025 08:09:11 -0400 Subject: [PATCH 02/12] use MailboxAddress approach sugested by @jstedfast --- .../Infrastructure/Jobs/NotificationJob.cs | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) 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}>"; } From 9508ff68db7a88d901f2030486ffb2659091e002 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sun, 21 Sep 2025 14:29:05 +0200 Subject: [PATCH 03/12] nuspec files updated --- Oqtane.Package/Oqtane.Client.nuspec | 2 +- Oqtane.Package/Oqtane.Server.nuspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 @@ - + From 7fed6bb93ad67c1550748525a36fd698d2399083 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 21 Sep 2025 11:12:07 -0400 Subject: [PATCH 04/12] improve Profile ability to use Settings --- .../Modules/Admin/Profiles/Edit.razor | 53 +++++++++++++++++-- .../Modules/Admin/Profiles/Edit.resx | 8 ++- .../Controllers/SettingController.cs | 4 +- 3 files changed, 58 insertions(+), 7 deletions(-) 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/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.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); From e83d7e9d57dffc566b9f47793ac32b5690fa302f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ton=C4=87i=20Vatavuk?= Date: Tue, 23 Sep 2025 15:21:03 +0200 Subject: [PATCH 05/12] Fix XML comment in UnzipFileAsync method Removed an unnecessary XML comment parameter closing tag. --- Oqtane.Client/Services/FileService.cs | 1 - 1 file changed, 1 deletion(-) 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); } From 916019f0153e6065e7c565388fe1abb7140af387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ton=C4=87i=20Vatavuk?= Date: Tue, 23 Sep 2025 16:18:52 +0200 Subject: [PATCH 06/12] RadzenTextEditor.placeholder.cs in RadzenTextEditor for docs 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. --- .../Radzen/RadzenTextEditor.placeholder.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs 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; From 2cb568773c9e6530807d94463974680663c50661 Mon Sep 17 00:00:00 2001 From: David Montesinos <90258222+mdmontesinos@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:21:20 +0200 Subject: [PATCH 07/12] Restore order of SetImage in FileManager Fixes #5648 --- Oqtane.Client/Modules/Controls/FileManager.razor | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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(); } From 116d163b9d75dd7bb88801de343b269b48a8d7c9 Mon Sep 17 00:00:00 2001 From: Pieter Kuyck Date: Tue, 23 Sep 2025 22:54:34 +0200 Subject: [PATCH 08/12] Update Oqtane.Application.Server.csproj Add Package reference that will create the BlazorDebugProxy folder. --- Oqtane.Application/Server/Oqtane.Application.Server.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 71dbe6a0..64822540 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -17,7 +17,8 @@ - + + From 8d23d9aba3626b672162576fa01682f175130fdb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 25 Sep 2025 13:55:02 -0400 Subject: [PATCH 09/12] allow themes to define usage permissions similar to modules --- .../Admin/ModuleDefinitions/Edit.razor | 7 +- Oqtane.Client/Modules/Admin/Pages/Add.razor | 12 +- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 12 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 12 +- Oqtane.Client/Modules/Admin/Sites/Add.razor | 2 +- Oqtane.Client/Modules/Admin/Themes/Add.razor | 2 +- Oqtane.Client/Modules/Admin/Themes/Edit.razor | 171 ++++++++++-------- .../Modules/Admin/Themes/Index.razor | 4 +- .../Modules/Admin/ModuleDefinitions/Edit.resx | 4 +- .../Resources/Modules/Admin/Themes/Edit.resx | 6 + Oqtane.Client/Services/ThemeService.cs | 16 +- .../Controllers/ModuleDefinitionController.cs | 2 +- Oqtane.Server/Controllers/ThemeController.cs | 63 +++++-- .../Repository/ModuleDefinitionRepository.cs | 26 +-- Oqtane.Server/Repository/SiteRepository.cs | 2 +- Oqtane.Server/Repository/ThemeRepository.cs | 80 +++++++- Oqtane.Server/Services/SiteService.cs | 2 +- Oqtane.Shared/Models/Theme.cs | 3 + 18 files changed, 296 insertions(+), 130 deletions(-) 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/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/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/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/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.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/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/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; } From 2db1fe0890d2ff623cbe46123d03fe773bd723d6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 25 Sep 2025 14:09:41 -0400 Subject: [PATCH 10/12] add all direct package dependencies to Application Template --- .../Client/Oqtane.Application.Client.csproj | 41 +++++++++++-------- .../Server/Oqtane.Application.Server.csproj | 39 ++++++++++-------- .../Shared/Oqtane.Application.Shared.csproj | 21 ++++++---- 3 files changed, 58 insertions(+), 43 deletions(-) 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/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 64822540..aaf443fa 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -1,24 +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/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 + - - - + + + + + + + From 5420f625b423f3076774bc80e753bb42761c9c06 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 25 Sep 2025 14:42:40 -0400 Subject: [PATCH 11/12] changes to template.json based on https://github.com/sayedihashimi/template-sample --- .../.template.config/template.json | 77 +++++++++++++++++-- .../Server/Properties/launchSettings.json | 4 +- 2 files changed, 74 insertions(+), 7 deletions(-) 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/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" } From ec00b1162fe267b20e787111b5f217ac9eba9fd0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 25 Sep 2025 14:46:51 -0400 Subject: [PATCH 12/12] update README --- Oqtane.Application/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.