From 14b0d7abf04803daecce7e46a37af7609f4833c2 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 2 May 2025 12:16:55 +0200 Subject: [PATCH 01/56] Updated to Bootstrap 5 Updated to Bootstrap 5.3.5 Update bootswatch Cyborg to 5.3.5 using https://cdn.jsdelivr.net because it is not available at https://cdnjs.com/libraries --- Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs | 4 ++-- Oqtane.Shared/Shared/Constants.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs b/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs index f0197b71..f03d7302 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs +++ b/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs @@ -16,8 +16,8 @@ namespace Oqtane.Themes.OqtaneTheme ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client", Resources = new List() { - // obtained from https://cdnjs.com/libraries - new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"), + // obtained from https://www.jsdelivr.com/package/npm/bootswatch + new Stylesheet("https://cdn.jsdelivr.net/npm/bootswatch@5.3.5/dist/cyborg/bootstrap.min.css"), new Stylesheet("~/Theme.css"), new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 6e70f5bc..bae50ff0 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -86,10 +86,10 @@ namespace Oqtane.Shared public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" }; public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client"; - public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"; - public const string BootstrapScriptIntegrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg=="; - public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"; - public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg=="; + public const string BootstrapScriptUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"; + public const string BootstrapScriptIntegrity = "sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq"; + public const string BootstrapStylesheetUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"; + public const string BootstrapStylesheetIntegrity = "sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"; public const string CookieConsentCookieName = "Oqtane.CookieConsent"; public const string CookieConsentCookieValue = "yes"; From d81514e9bedcd8b653467c710347ea9b50fe3ba2 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 2 May 2025 12:19:58 +0200 Subject: [PATCH 02/56] Update for Blazor Theme --- Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor index 1622c5e7..e3b11560 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor @@ -37,7 +37,7 @@ public override List Resources => new List() { // obtained from https://cdnjs.com/libraries - new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", "anonymous"), + new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), new Stylesheet(ThemePath() + "Theme.css"), new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") }; From 3811b8f0c0622d4fde5934608f81612d1dfc285f Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 7 May 2025 11:46:07 +0200 Subject: [PATCH 03/56] Theme Template updated --- .../wwwroot/Themes/Templates/External/Client/ThemeInfo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs index 9bbfed91..d4d395cd 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -16,10 +16,10 @@ namespace [Owner].Theme.[Theme] ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane", Resources = new List() { - // obtained from https://cdnjs.com/libraries - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", CrossOrigin = "anonymous" }, + // obtained from https://www.jsdelivr.com/ + new Script(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js", Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==", CrossOrigin = "anonymous" } + new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") } }; From 11150b6a1049da488bab3d34a3da25386bb32a49 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 29 May 2025 17:03:20 -0400 Subject: [PATCH 04/56] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a69b4390..10112a56 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) was released on April 10, 2025 and is a maintenance release including 41 pull requests by 3 different contributors, pushing the total number of project commits all-time to over 6500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) was released on May 29, 2025 and is a maintenance release including 59 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 6600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -22,11 +22,11 @@ Microsoft's Public Cloud (requires an Azure account) A free ASP.NET hosting account. No hidden fees. No credit card required. [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) -# Getting Started (Version 6.1.2) +# Getting Started (Version 6) **Installing using source code from the Dev/Master branch:** -- Install **[.NET 9.0.4 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. @@ -92,6 +92,9 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025) +- [x] Stabilization improvements + [6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025) - [x] Stabilization improvements From 985e50d41587c09a618fc07fff678d0c6fd683a4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 May 2025 17:04:45 -0400 Subject: [PATCH 05/56] update Azure ARM template to 6.1.3 --- azuredeploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredeploy.json b/azuredeploy.json index e57cc1f8..d7970211 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -220,7 +220,7 @@ "apiVersion": "2024-04-01", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "properties": { - "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.2/Oqtane.Framework.6.1.2.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" From c13ce3d0f14580cd2c22badc35bcd91811faffea Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 3 Jun 2025 15:24:43 +0200 Subject: [PATCH 06/56] Update Index.razor Deprecated .text-muted will be replaced by .text-body-secondary in v6. --- Oqtane.Client/Modules/Admin/SearchResults/Index.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/SearchResults/Index.razor b/Oqtane.Client/Modules/Admin/SearchResults/Index.razor index 80de5e11..bb58a9c1 100644 --- a/Oqtane.Client/Modules/Admin/SearchResults/Index.razor +++ b/Oqtane.Client/Modules/Admin/SearchResults/Index.razor @@ -46,7 +46,7 @@

@context.Title

-

@((MarkupString)context.Snippet)

+

@((MarkupString)context.Snippet)

From f776977af811d2d79f22875204e75d5cf1023a74 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 4 Jun 2025 13:28:49 +0200 Subject: [PATCH 07/56] Server References Updated update SixLabors.ImageSharp update Swashbuckle.AspNetCore --- Oqtane.Server/Oqtane.Server.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 2068c0ea..56a682a9 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -46,9 +46,9 @@ - + - + From 4418e27c290b7e449753f4a842607046711585bb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Jun 2025 09:31:54 -0400 Subject: [PATCH 08/56] rendering optimizations --- .../Controls/Container/ModuleActions.razor | 1 + .../Themes/Controls/Theme/ControlPanel.razor | 1 + Oqtane.Client/UI/ContainerBuilder.razor | 2 +- Oqtane.Client/UI/ModuleInstance.razor | 10 ++++--- Oqtane.Client/UI/PageState.cs | 29 +++++++++++++++++++ Oqtane.Client/UI/Pane.razor | 19 ++++-------- Oqtane.Client/UI/RenderModeBoundary.razor | 4 +-- Oqtane.Client/UI/Routes.razor | 16 ++++++---- Oqtane.Client/UI/SiteRouter.razor | 25 ++++++++++++---- 9 files changed, 74 insertions(+), 33 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor b/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor index 4b973c48..8672c85a 100644 --- a/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor +++ b/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor @@ -20,6 +20,7 @@ protected override void OnParametersSet() { // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries + // only include properties required by the ModuleActionsInteractive component _pageState = new PageState { Alias = PageState.Alias, diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 717cc31e..f38a66c3 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -91,6 +91,7 @@ } // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries + // only include properties required by the ControlPanelInteractive component _pageState = new PageState { Alias = PageState.Alias, diff --git a/Oqtane.Client/UI/ContainerBuilder.razor b/Oqtane.Client/UI/ContainerBuilder.razor index 6a769aed..76565c6b 100644 --- a/Oqtane.Client/UI/ContainerBuilder.razor +++ b/Oqtane.Client/UI/ContainerBuilder.razor @@ -6,7 +6,7 @@ @if (ComponentType != null && _visible) { - + @if (_useadminborder) {
diff --git a/Oqtane.Client/UI/ModuleInstance.razor b/Oqtane.Client/UI/ModuleInstance.razor index e9a634d3..0fda480f 100644 --- a/Oqtane.Client/UI/ModuleInstance.razor +++ b/Oqtane.Client/UI/ModuleInstance.razor @@ -10,11 +10,11 @@ @((MarkupString)_comment) @if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static) { - + } else { - + } } @if (PageState.ModuleId == -1) @@ -32,6 +32,7 @@ private bool _prerender; private string _comment; + private PageState _pageState; protected override void OnParametersSet() { @@ -48,11 +49,12 @@ } _comment += " -->"; + _pageState = PageState.Clone(); if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive) { // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries - // please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components - PageState.Site.Pages = new List(); + // please note that this performance optimization results in the PageState.Pages property not being available for use in downstream Interactive components + _pageState.Site.Pages = new List(); } } diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs index a038a81e..91cf158c 100644 --- a/Oqtane.Client/UI/PageState.cs +++ b/Oqtane.Client/UI/PageState.cs @@ -37,5 +37,34 @@ namespace Oqtane.UI { get { return Site?.Languages; } } + + public PageState Clone() + { + return new PageState + { + Alias = Alias, + Site = Site, + Page = Page, + Modules = Modules, + User = User, + Uri = Uri, + Route = Route, + QueryString = QueryString, + UrlParameters = UrlParameters, + ModuleId = ModuleId, + Action = Action, + EditMode = EditMode, + LastSyncDate = LastSyncDate, + RenderMode = RenderMode, + Runtime = Runtime, + VisitorId = VisitorId, + RemoteIPAddress = RemoteIPAddress, + ReturnUrl = ReturnUrl, + IsInternalNavigation = IsInternalNavigation, + RenderId = RenderId, + Refresh = Refresh, + AllowCookies = AllowCookies + }; + } } } diff --git a/Oqtane.Client/UI/Pane.razor b/Oqtane.Client/UI/Pane.razor index 70226c2e..633cd4fd 100644 --- a/Oqtane.Client/UI/Pane.razor +++ b/Oqtane.Client/UI/Pane.razor @@ -29,7 +29,7 @@ else RenderFragment DynamicComponent { get; set; } protected override void OnParametersSet() - { + { if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction) { _useadminborder = true; @@ -45,12 +45,6 @@ else { foreach (Module module in PageState.Modules) { - // set renderid - this allows the framework to determine which components should be rendered when PageState changes - if (module.RenderId != PageState.RenderId) - { - module.RenderId = PageState.RenderId; - } - var pane = module.Pane; if (module.ModuleId == PageState.ModuleId && PageState.Action != Constants.DefaultAction) { @@ -101,7 +95,7 @@ else if (authorized) { - CreateComponent(builder, module, module.PageModuleId); + CreateComponent(builder, module); } } } @@ -112,7 +106,7 @@ else // check if user is authorized to view module if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList)) { - CreateComponent(builder, module, -1); + CreateComponent(builder, module); } } } @@ -121,14 +115,11 @@ else }; } - private void CreateComponent(RenderTreeBuilder builder, Module module, int key) + private void CreateComponent(RenderTreeBuilder builder, Module module) { builder.OpenComponent(0, typeof(ContainerBuilder)); builder.AddAttribute(1, "ModuleState", module); - if (key != -1) - { - builder.SetKey(module.PageModuleId); - } + builder.SetKey(module.PageModuleId); builder.CloseComponent(); } } diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor index 2c451f29..dbaac7f5 100644 --- a/Oqtane.Client/UI/RenderModeBoundary.razor +++ b/Oqtane.Client/UI/RenderModeBoundary.razor @@ -4,8 +4,8 @@ @inject ILogService LoggingService @inherits ErrorBoundary - - + + @if (CurrentException is null) { @if (ModuleType != null) diff --git a/Oqtane.Client/UI/Routes.razor b/Oqtane.Client/UI/Routes.razor index 20dc0a09..6081966c 100644 --- a/Oqtane.Client/UI/Routes.razor +++ b/Oqtane.Client/UI/Routes.razor @@ -48,12 +48,18 @@ private bool _initialized = false; private bool _installed = false; - private string _display = "display: none;"; + private string _display = ""; private PageState _pageState { get; set; } protected override async Task OnParametersSetAsync() { + if (PageState != null && PageState.RenderMode == RenderModes.Interactive && PageState.Site.Prerender) + { + // prevents flash on initial interactive page load when using prerendering + _display = "display: none;"; + } + SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AuthorizationToken = AuthorizationToken; SiteState.Platform = Platform; @@ -61,7 +67,7 @@ if (Runtime == Runtimes.Hybrid) { - var installation = await InstallationService.IsInstalled(); + var installation = await InstallationService.IsInstalled(); _installed = installation.Success; if (installation.Alias != null) { @@ -73,8 +79,8 @@ if (PageState != null) { _pageState = PageState; - SiteState.Alias = PageState.Alias; - SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : ""; + SiteState.Alias = _pageState.Alias; + SiteState.RemoteIPAddress = _pageState.RemoteIPAddress; _installed = true; } } @@ -85,9 +91,7 @@ { if (firstRender) { - // prevents flash on initial interactive page load _display = ""; - StateHasChanged(); } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 3c3c81fe..a345a7f0 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -71,7 +71,7 @@ { if (PageState == null || PageState.Refresh) { - await Refresh(); + await Refresh(false); } } @@ -79,7 +79,7 @@ { _absoluteUri = args.Location; _isInternalNavigation = true; - await Refresh(); + await Refresh(true); } Task IHandleAfterRender.OnAfterRenderAsync() @@ -93,7 +93,7 @@ } [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - private async Task Refresh() + private async Task Refresh(bool locationChanged) { Site site = null; Page page = null; @@ -103,6 +103,7 @@ var refresh = false; var lastsyncdate = DateTime.MinValue; var visitorId = -1; + var renderid = Guid.Empty; _error = ""; Route route = new Route(_absoluteUri, SiteState.Alias.Path); @@ -288,11 +289,21 @@ modules = PageState.Modules; } + // renderid allows the framework to determine which module components should be rendered on a page + if (PageState == null || locationChanged) + { + renderid = Guid.NewGuid(); + } + else + { + renderid = PageState.RenderId; + } + // load additional metadata for current page page = ProcessPage(page, site, user, SiteState.Alias, action); // load additional metadata for modules - (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); + (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias, renderid); //cookie consent var _allowCookies = PageState?.AllowCookies; @@ -324,7 +335,7 @@ RemoteIPAddress = SiteState.RemoteIPAddress, ReturnUrl = returnurl, IsInternalNavigation = _isInternalNavigation, - RenderId = Guid.NewGuid(), + RenderId = renderid, Refresh = false, AllowCookies = _allowCookies.GetValueOrDefault(true) }; @@ -447,7 +458,7 @@ return page; } - private (Page Page, List Modules) ProcessModules(Site site, Page page, List modules, int moduleid, string action, string defaultcontainertype, Alias alias) + private (Page Page, List Modules) ProcessModules(Site site, Page page, List modules, int moduleid, string action, string defaultcontainertype, Alias alias, Guid renderid) { var paneindex = new Dictionary(); @@ -592,6 +603,8 @@ { module.ContainerType = defaultcontainertype; } + + module.RenderId = renderid; } foreach (Module module in modules.Where(item => item.PageId == page.PageId)) From 85085bf4c7c29f8f32b6ae8959f4f36fe99c689f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Jun 2025 10:37:25 -0400 Subject: [PATCH 09/56] stop gap fix to mitigate date conversion exceptions on WebAssembly --- Oqtane.Client/Modules/ModuleBase.cs | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 39681067..0dabafd3 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -500,17 +500,24 @@ namespace Oqtane.Modules }; } - // date methods + // date conversion methods public DateTime? UtcToLocal(DateTime? datetime) { TimeZoneInfo timezone = null; - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + try { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + catch { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + // The time zone ID was not found on the local computer } return Utilities.UtcAsLocalDateTime(datetime, timezone); } @@ -518,13 +525,20 @@ namespace Oqtane.Modules public DateTime? LocalToUtc(DateTime? datetime) { TimeZoneInfo timezone = null; - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + try { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + catch { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + // The time zone ID was not found on the local computer } return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); } From d4f0805108f8dd5164539d00cc08bd6740356056 Mon Sep 17 00:00:00 2001 From: David Montesinos <90258222+mdmontesinos@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:05:40 +0200 Subject: [PATCH 10/56] fix #5352: remove requests to cookie consent service when not enabled --- Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index 66626cbc..207de910 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -103,6 +103,9 @@ { var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty); _enabled = !string.IsNullOrEmpty(cookieConsentSetting); + + if (!_enabled) return; + _optout = cookieConsentSetting == "optout"; _actioned = await CookieConsentService.IsActionedAsync(); @@ -164,4 +167,4 @@ StateHasChanged(); } } -} \ No newline at end of file +} From ff450ca43a44db405b46ebcbc2ffcb2fddbee764 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Mon, 9 Jun 2025 10:29:43 +0200 Subject: [PATCH 11/56] Fix for Scheduled Jobs UI #5354 This PR addresses an issue where null date/time values could cause exceptions when processing job scheduling. Changes Made: - Added proper null checks for _startDate, _startTime, _endDate, _endTime, _nextDate, and _nextTime - Improved parsing safety for _retentionHistory using int.TryParse() - Added validation to fail early with meaningful error messages Impact: Prevents NullReferenceException and InvalidOperationException when date/time fields are missing --- Oqtane.Client/Modules/Admin/Jobs/Edit.razor | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index bcb500ff..f0ff0a2c 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -176,10 +176,18 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)); - job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)); - job.RetentionHistory = int.Parse(_retentionHistory); - job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)); + job.StartDate = _startDate.HasValue && _startTime.HasValue + ? LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)) + : null; + + job.EndDate = _endDate.HasValue && _endTime.HasValue + ? LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)) + : null; + + job.NextExecution = _nextDate.HasValue && _nextTime.HasValue + ? LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)) + : null; + job.RetentionHistory = int.Parse(_retentionHistory); try { From 1412737036a03cc4ef78fa88426aec62dd94b71f Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 10 Jun 2025 12:27:55 +0200 Subject: [PATCH 12/56] Date / Time validations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR ensures time fields are required when dates are set, using Oqtane validation and dynamically toggles the required attribute on time inputs when their corresponding date fields have values. Benefits: - Uses Oqtane's validation for a polished UX. - Reduces custom validation code. - Aligns with our internal form logic. - Tested across all date/time scenarios—works flawlessly! **Testing Confirmed:** - Date + Time Provided → Saves successfully. - No Date + No Time → Optional (no validation). - Date + No Time → Browser blocks submission with icon error. --- Oqtane.Client/Modules/Admin/Jobs/Edit.razor | 27 ++++++++++----------- Oqtane.Client/Modules/ModuleBase.cs | 10 ++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index f0ff0a2c..26d96688 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -56,7 +56,7 @@
- +
@@ -69,7 +69,7 @@
- +
@@ -82,7 +82,7 @@
- +
@@ -176,18 +176,18 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = _startDate.HasValue && _startTime.HasValue - ? LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)) - : null; + job.StartDate = _startDate.HasValue && _startTime.HasValue + ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay)) + : null; - job.EndDate = _endDate.HasValue && _endTime.HasValue - ? LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)) - : null; + job.EndDate = _endDate.HasValue && _endTime.HasValue + ? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay)) + : null; - job.NextExecution = _nextDate.HasValue && _nextTime.HasValue - ? LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)) - : null; - job.RetentionHistory = int.Parse(_retentionHistory); + job.NextExecution = _nextDate.HasValue && _nextTime.HasValue + ? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay)) + : null; + job.RetentionHistory = int.Parse(_retentionHistory); try { @@ -206,5 +206,4 @@ AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); } } - } diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 0dabafd3..a066d159 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -503,6 +503,10 @@ namespace Oqtane.Modules // date conversion methods public DateTime? UtcToLocal(DateTime? datetime) { + // Early return if input is null + if (datetime == null) + return null; + TimeZoneInfo timezone = null; try { @@ -519,11 +523,16 @@ namespace Oqtane.Modules { // The time zone ID was not found on the local computer } + return Utilities.UtcAsLocalDateTime(datetime, timezone); } public DateTime? LocalToUtc(DateTime? datetime) { + // Early return if input is null + if (datetime == null) + return null; + TimeZoneInfo timezone = null; try { @@ -540,6 +549,7 @@ namespace Oqtane.Modules { // The time zone ID was not found on the local computer } + return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); } From b63590d6c7a1a3c01c49de3aea7bd202bc31d8cb Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 3 Jul 2025 15:42:11 +0800 Subject: [PATCH 13/56] Fix #5363: update SettingService.MergeSettings. --- .../Services/Interfaces/ISettingService.cs | 2 +- Oqtane.Client/Services/SettingService.cs | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Oqtane.Client/Services/Interfaces/ISettingService.cs b/Oqtane.Client/Services/Interfaces/ISettingService.cs index dcdc7386..2f4c4b94 100644 --- a/Oqtane.Client/Services/Interfaces/ISettingService.cs +++ b/Oqtane.Client/Services/Interfaces/ISettingService.cs @@ -256,7 +256,7 @@ namespace Oqtane.Services Dictionary SetSetting(Dictionary settings, string settingName, string settingValue, bool isPrivate); - Dictionary MergeSettings(Dictionary settings1, Dictionary settings2); + Dictionary MergeSettings(Dictionary baseSettings, Dictionary overwriteSettings); [Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)] diff --git a/Oqtane.Client/Services/SettingService.cs b/Oqtane.Client/Services/SettingService.cs index 4a43972e..a55eeeba 100644 --- a/Oqtane.Client/Services/SettingService.cs +++ b/Oqtane.Client/Services/SettingService.cs @@ -266,27 +266,25 @@ namespace Oqtane.Services return settings; } - public Dictionary MergeSettings(Dictionary settings1, Dictionary settings2) + public Dictionary MergeSettings(Dictionary baseSettings, Dictionary overwriteSettings) { - if (settings1 == null) + var settings = baseSettings != null ? new Dictionary(baseSettings) : new Dictionary(); + if (overwriteSettings != null) { - settings1 = new Dictionary(); - } - if (settings2 != null) - { - foreach (var setting in settings2) + foreach (var setting in overwriteSettings) { - if (settings1.ContainsKey(setting.Key)) + if (settings.ContainsKey(setting.Key)) { - settings1[setting.Key] = setting.Value; + settings[setting.Key] = setting.Value; } else { - settings1.Add(setting.Key, setting.Value); + settings.Add(setting.Key, setting.Value); } } } - return settings1; + + return settings; } [Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)] From 0d3d6937995907fbab8ab582c5c9e56e7a6b80f2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 3 Jul 2025 16:44:59 -0400 Subject: [PATCH 14/56] fix #5374 Visitor Settings not returned due to change in Visitor cookie format --- Oqtane.Server/Components/App.razor | 1 + .../Controllers/SettingController.cs | 22 +++++++++---------- .../Controllers/VisitorController.cs | 14 +++++++----- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 570e4426..638c8031 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -345,6 +345,7 @@ DateTime expiry = DateTime.MinValue; if (visitorCookieValue != null && visitorCookieValue.Contains("|")) { + // visitor cookies contain the visitor id and an expiry date separated by a pipe symbol var values = visitorCookieValue.Split('|'); int.TryParse(values[0], out _visitorId); DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry); diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index fd8708c1..3c67887b 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -70,7 +70,6 @@ namespace Oqtane.Controllers _identityOptionsMonitorCache = identityOptionsMonitorCache; _logger = logger; _alias = tenantManager.GetAlias(); - _visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); } // GET: api/ @@ -299,11 +298,8 @@ namespace Oqtane.Controllers authorized = User.IsInRole(RoleNames.Admin); if (!authorized) { - // a visitor may have cookies disabled - if (int.TryParse(Request.Cookies[_visitorCookie], out int visitorId)) - { - authorized = (visitorId == entityId); - } + var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); + authorized = (entityId == GetVisitorCookieId(Request.Cookies[visitorCookieName])); } break; default: // custom entity @@ -344,11 +340,8 @@ namespace Oqtane.Controllers case EntityNames.Visitor: if (!User.IsInRole(RoleNames.Admin)) { - filter = true; - if (int.TryParse(Request.Cookies[_visitorCookie], out int visitorId)) - { - filter = (visitorId != entityId); - } + var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); + filter = (entityId != GetVisitorCookieId(Request.Cookies[visitorCookieName])); } break; default: // custom entity @@ -358,6 +351,13 @@ namespace Oqtane.Controllers return filter; } + private int GetVisitorCookieId(string visitorCookie) + { + // visitor cookies contain the visitor id and an expiry date separated by a pipe symbol + visitorCookie = (visitorCookie.Contains("|")) ? visitorCookie.Split('|')[0] : visitorCookie; + return (int.TryParse(visitorCookie, out int visitorId)) ? visitorId : -1; + } + private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action) { _syncManager.AddSyncEvent(_alias, EntityName + "Setting", SettingId, Action); diff --git a/Oqtane.Server/Controllers/VisitorController.cs b/Oqtane.Server/Controllers/VisitorController.cs index 66ba447c..e0074452 100644 --- a/Oqtane.Server/Controllers/VisitorController.cs +++ b/Oqtane.Server/Controllers/VisitorController.cs @@ -51,11 +51,8 @@ namespace Oqtane.Controllers bool authorized = User.IsInRole(RoleNames.Admin); if (!authorized) { - var visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); - if (int.TryParse(Request.Cookies[visitorCookie], out int visitorId)) - { - authorized = (visitorId == id); - } + var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); + authorized = (id == GetVisitorCookieId(Request.Cookies[visitorCookieName])); } var visitor = _visitors.GetVisitor(id); @@ -77,5 +74,12 @@ namespace Oqtane.Controllers return null; } } + + private int GetVisitorCookieId(string visitorCookie) + { + // visitor cookies contain the visitor id and an expiry date separated by a pipe symbol + visitorCookie = (visitorCookie.Contains("|")) ? visitorCookie.Split('|')[0] : visitorCookie; + return (int.TryParse(visitorCookie, out int visitorId)) ? visitorId : -1; + } } } From 711de49571d62d39be042803d1aea109b225596e Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Fri, 4 Jul 2025 12:55:40 +0200 Subject: [PATCH 15/56] feat: replace System.Net.Mail with MailKit (#5372) --- .../Infrastructure/Jobs/NotificationJob.cs | 58 ++++++++++--------- Oqtane.Server/Oqtane.Server.csproj | 1 + 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 4147455b..57525d27 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Net.Mail; + +using MailKit.Net.Smtp; + using Microsoft.Extensions.DependencyInjection; + +using MimeKit; + using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; @@ -48,18 +52,17 @@ namespace Oqtane.Infrastructure settingRepository.GetSettingValue(settings, "SMTPPort", "") != "" && settingRepository.GetSettingValue(settings, "SMTPSender", "") != "") { - // construct SMTP Client - var client = new SmtpClient() - { - DeliveryMethod = SmtpDeliveryMethod.Network, - UseDefaultCredentials = false, - Host = settingRepository.GetSettingValue(settings, "SMTPHost", ""), - Port = int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), - EnableSsl = bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) - }; + // construct SMTP Client + using var client = new SmtpClient(); + + client.Connect(host: settingRepository.GetSettingValue(settings, "SMTPHost", ""), + port: int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), + options: MailKit.Security.SecureSocketOptions.Auto); + if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") { - client.Credentials = new NetworkCredential(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + client.Authenticate(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), + settingRepository.GetSettingValue(settings, "SMTPPassword", "")); } // iterate through undelivered notifications @@ -88,7 +91,7 @@ namespace Oqtane.Infrastructure } // validate recipient - if (string.IsNullOrEmpty(notification.ToEmail) || !MailAddress.TryCreate(notification.ToEmail, out _)) + if (string.IsNullOrEmpty(notification.ToEmail) || !MailboxAddress.TryParse(notification.ToEmail, out _)) { log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}
"; notification.IsDeleted = true; @@ -96,50 +99,52 @@ namespace Oqtane.Infrastructure } else { - MailMessage mailMessage = new MailMessage(); + MimeMessage mailMessage = new MimeMessage(); // sender if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail)) { if (!string.IsNullOrEmpty(notification.FromDisplayName)) { - mailMessage.From = new MailAddress(notification.FromEmail, notification.FromDisplayName); + mailMessage.From.Add(new MailboxAddress(notification.FromDisplayName, notification.FromEmail)); } else { - mailMessage.From = new MailAddress(notification.FromEmail); + mailMessage.From.Add(new MailboxAddress("", notification.FromEmail)); } } else { - mailMessage.From = new MailAddress(settingRepository.GetSettingValue(settings, "SMTPSender", ""), (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name); + mailMessage.From.Add(new MailboxAddress((!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name, + settingRepository.GetSettingValue(settings, "SMTPSender", ""))); } // recipient if (!string.IsNullOrEmpty(notification.ToDisplayName)) { - mailMessage.To.Add(new MailAddress(notification.ToEmail, notification.ToDisplayName)); + mailMessage.To.Add(new MailboxAddress(notification.ToDisplayName, notification.ToEmail)); } else { - mailMessage.To.Add(new MailAddress(notification.ToEmail)); + mailMessage.To.Add(new MailboxAddress("", notification.ToEmail)); } // subject mailMessage.Subject = notification.Subject; //body - mailMessage.Body = notification.Body; - if (!mailMessage.Body.Contains("<") || !mailMessage.Body.Contains(">")) + var bodyText = notification.Body; + + if (!bodyText.Contains('<') || !bodyText.Contains('>')) { // plain text messages should convert line breaks to HTML tags to preserve formatting - mailMessage.Body = mailMessage.Body.Replace("\n", "
"); + bodyText = bodyText.Replace("\n", "
"); } - // encoding - mailMessage.SubjectEncoding = System.Text.Encoding.UTF8; - mailMessage.BodyEncoding = System.Text.Encoding.UTF8; - mailMessage.IsBodyHtml = true; + mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8) + { + Text = bodyText + }; // send mail try @@ -157,6 +162,7 @@ namespace Oqtane.Infrastructure } } } + client.Disconnect(true); log += "Notifications Delivered: " + sent + "
"; } else diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 56a682a9..c67e6a71 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -34,6 +34,7 @@
+ From 6b567364f9fcdfad6fd6e4c7488f1e3a0a2e6b62 Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Fri, 4 Jul 2025 14:55:02 +0200 Subject: [PATCH 16/56] feat: use appropriate UseSSL equivalent in MailKit --- Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 57525d27..e1d3e17a 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -57,7 +57,7 @@ namespace Oqtane.Infrastructure client.Connect(host: settingRepository.GetSettingValue(settings, "SMTPHost", ""), port: int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), - options: MailKit.Security.SecureSocketOptions.Auto); + options: bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? MailKit.Security.SecureSocketOptions.StartTls : MailKit.Security.SecureSocketOptions.None); if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") { From cb5e4e076f5c23a3c2d22098962ea5151d1b4599 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 7 Jul 2025 12:42:35 -0400 Subject: [PATCH 17/56] remove unused variable --- Oqtane.Server/Controllers/SettingController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 3c67887b..2c33c958 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -43,7 +43,6 @@ namespace Oqtane.Controllers private readonly ILogManager _logger; private readonly Alias _alias; - private readonly string _visitorCookie; public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IOptions cookieOptions, IOptionsSnapshot cookieOptionsSnapshot, IOptionsMonitorCache cookieOptionsMonitorCache, From ac236607f5d24b78febdef63fdb30b0a36628c31 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 13:09:10 -0400 Subject: [PATCH 18/56] update to .NET SDK 9.0.6 --- Oqtane.Client/Oqtane.Client.csproj | 8 ++++---- .../Oqtane.Database.PostgreSQL.csproj | 2 +- .../Oqtane.Database.SqlServer.csproj | 2 +- .../Oqtane.Database.Sqlite.csproj | 2 +- Oqtane.Server/Oqtane.Server.csproj | 20 +++++++++---------- Oqtane.Shared/Oqtane.Shared.csproj | 8 ++++---- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index bb7671d6..32c7deda 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 1238c9cb..3535cf59 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -34,7 +34,7 @@ - + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index 6a594463..485ee95e 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index c13db7c9..4f60e7ed 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index c67e6a71..55ca2742 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -34,22 +34,22 @@ - - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + - + + diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index fbffe080..13eaaf2e 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -19,11 +19,11 @@ - - - + + + - + From 85a376b17d4efb88c16129f982c85af788698963 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 13:11:52 -0400 Subject: [PATCH 19/56] update to .NET SDK 9.0.6 --- Oqtane.Maui/Oqtane.Maui.csproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 6f0b4c92..8706acd4 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -67,14 +67,14 @@ - - - - - - - - + + + + + + + + From 668e0cb4ebf5110e15b76661bcf2258b1d08d3ec Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 13:14:53 -0400 Subject: [PATCH 20/56] update to .NET SDK 9.0.6 --- .../Client/[Owner].Module.[Module].Client.csproj | 10 +++++----- .../Server/[Owner].Module.[Module].Server.csproj | 8 ++++---- .../Client/[Owner].Theme.[Theme].Client.csproj | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index c40f7c41..688f5514 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 75da4858..a0610d56 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index 77ae8f52..2bfe83d8 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@ - - - + + + From 17045073c81f5f640aae96147e127f8dee11fb23 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 13:20:28 -0400 Subject: [PATCH 21/56] bump version to 6.1.4 --- Oqtane.Client/Oqtane.Client.csproj | 4 ++-- Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj | 4 ++-- .../Oqtane.Database.PostgreSQL.csproj | 4 ++-- Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj | 4 ++-- Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj | 4 ++-- Oqtane.Maui/Oqtane.Maui.csproj | 6 +++--- Oqtane.Package/Oqtane.Client.nuspec | 4 ++-- Oqtane.Package/Oqtane.Framework.nuspec | 6 +++--- Oqtane.Package/Oqtane.Server.nuspec | 4 ++-- Oqtane.Package/Oqtane.Shared.nuspec | 4 ++-- Oqtane.Package/Oqtane.Updater.nuspec | 4 ++-- Oqtane.Package/install.ps1 | 2 +- Oqtane.Package/upgrade.ps1 | 2 +- Oqtane.Server/Oqtane.Server.csproj | 4 ++-- Oqtane.Shared/Oqtane.Shared.csproj | 4 ++-- Oqtane.Shared/Shared/Constants.cs | 4 ++-- Oqtane.Updater/Oqtane.Updater.csproj | 4 ++-- 17 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 32c7deda..2b4fda7f 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net9.0 Exe Debug;Release - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index e6703b08..beedbc8e 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 3535cf59..330bf115 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index 485ee95e..98941039 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net9.0 - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 4f60e7ed..104c2cbd 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net9.0 - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 8706acd4..7f2170fd 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -30,7 +30,7 @@ com.oqtane.maui - 6.1.3 + 6.1.4 1 diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index b50b816c..6ff7ced3 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 6.1.3 + 6.1.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index a9e739dc..919e7025 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 6.1.3 + 6.1.4 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/download/v6.1.4/Oqtane.Framework.6.1.4.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index b5e58742..c0254edc 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 6.1.3 + 6.1.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 9c2041be..5b94b83b 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 6.1.3 + 6.1.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 09453309..b94833cf 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 6.1.3 + 6.1.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index fb0b6d7f..6426442d 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.4.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index e2f26321..958e0029 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.4.Upgrade.zip" -Force diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 55ca2742..b23e57d4 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 13eaaf2e..77dc05d9 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index c109af88..10be4133 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "6.1.3"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3"; + public static readonly string Version = "6.1.4"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 7474506c..57fab560 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net9.0 Exe - 6.1.3 + 6.1.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4 https://github.com/oqtane/oqtane.framework Git Oqtane From 461330773abea1ac09f7f12ab822b8e91db5d1c4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 16:04:19 -0400 Subject: [PATCH 22/56] resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user --- Oqtane.Client/Modules/Admin/UserProfile/Index.razor | 5 ----- .../Resources/Modules/Admin/UserProfile/Index.resx | 3 --- Oqtane.Client/UI/ThemeBuilder.razor | 7 ------- .../OqtaneSiteAuthenticationBuilderExtensions.cs | 4 ++-- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 2517c538..377b27d8 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -414,11 +414,6 @@ _displayname = PageState.User.DisplayName; _timezoneid = PageState.User.TimeZoneId; - if (string.IsNullOrEmpty(_email)) - { - AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning); - } - // get user folder var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); if (folder != null) diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index d6136ee8..2a6862bb 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -147,9 +147,6 @@ Current User Is Not Logged In - - You Must Provide An Email Address For Your User Account - Error Loading User Profile diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 6981b983..d22b21b4 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -20,13 +20,6 @@ return; } - // force authenticated user to provide email address (email may be missing if using external login) - if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile") - { - NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery))); - return; - } - // set page title if (!string.IsNullOrEmpty(PageState.Page.Title)) { diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index e98a0afa..f142c602 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -404,13 +404,13 @@ namespace Oqtane.Extensions else if (!string.IsNullOrEmpty(name)) // name claim provided { username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss"); - emailaddress = ""; // unknown - will need to be requested from user later + emailaddress = username + "@unknown.com"; displayname = name; } else // neither email nor name provided { username = Guid.NewGuid().ToString("N"); - emailaddress = ""; // unknown - will need to be requested from user later + emailaddress = username + "@unknown.com"; displayname = username; } From b0c1d36babc3cf99518bb2ea8f5dd035ccd342a6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 8 Jul 2025 16:27:35 -0400 Subject: [PATCH 23/56] update External Login default values for Facebook OAuth2 --- Oqtane.Shared/Shared/ExternalLoginProviders.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Oqtane.Shared/Shared/ExternalLoginProviders.cs b/Oqtane.Shared/Shared/ExternalLoginProviders.cs index 57cf9322..b368f83d 100644 --- a/Oqtane.Shared/Shared/ExternalLoginProviders.cs +++ b/Oqtane.Shared/Shared/ExternalLoginProviders.cs @@ -71,13 +71,15 @@ namespace Oqtane.Shared { "ExternalLogin:ProviderUrl", "https://developers.facebook.com" }, { "ExternalLogin:ProviderType", "oauth2" }, { "ExternalLogin:ProviderName", "Facebook" }, - { "ExternalLogin:AuthorizationUrl", "https://www.facebook.com/v18.0/dialog/oauth" }, - { "ExternalLogin:TokenUrl", "https://graph.facebook.com/v18.0/oauth/access_token" }, - { "ExternalLogin:UserInfoUrl", "https://graph.facebook.com/v18.0/me" }, + { "ExternalLogin:AuthorizationUrl", "https://www.facebook.com/v23.0/dialog/oauth" }, + { "ExternalLogin:TokenUrl", "https://graph.facebook.com/v23.0/oauth/access_token" }, + { "ExternalLogin:UserInfoUrl", "https://graph.facebook.com/v23.0/me?fields=id,name,email" }, { "ExternalLogin:ClientId", "YOUR CLIENT ID" }, { "ExternalLogin:ClientSecret", "YOUR CLIENT SECRET" }, - { "ExternalLogin:Scopes", "public_profile" }, - { "ExternalLogin:IdentifierClaimType", "id" } + { "ExternalLogin:Scopes", "public_profile,email" }, + { "ExternalLogin:IdentifierClaimType", "id" }, + { "ExternalLogin:NameClaimType", "name" }, + { "ExternalLogin:EmailClaimType", "email" } } } }; From 0a994afd675ba809379cd5aa7156c27503b87650 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 9 Jul 2025 02:52:30 +0200 Subject: [PATCH 24/56] Update References .NetCore 9.0.7 --- Oqtane.Client/Oqtane.Client.csproj | 8 ++++---- .../Oqtane.Database.PostgreSQL.csproj | 2 +- .../Oqtane.Database.SqlServer.csproj | 2 +- .../Oqtane.Database.Sqlite.csproj | 2 +- Oqtane.Server/Oqtane.Server.csproj | 16 ++++++++-------- .../Client/[Owner].Module.[Module].Client.csproj | 10 +++++----- .../Server/[Owner].Module.[Module].Server.csproj | 8 ++++---- .../Client/[Owner].Theme.[Theme].Client.csproj | 6 +++--- Oqtane.Shared/Oqtane.Shared.csproj | 8 ++++---- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 2b4fda7f..79410dec 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 330bf115..b0a8c50d 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -34,7 +34,7 @@ - + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index 98941039..8a8bfd66 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 104c2cbd..fb57225e 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index b23e57d4..139dcc96 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -34,21 +34,21 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 688f5514..8dff70b0 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index a0610d56..7d9645e0 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index 2bfe83d8..f3a6fe45 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@ - - - + + + diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 77dc05d9..c0d42f2e 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -19,11 +19,11 @@ - - - + + + - + From 13d9cb461b7db355c0767c7dbd06afb0762a8a7c Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 9 Jul 2025 03:42:26 +0200 Subject: [PATCH 25/56] Update Oqtane Maui project to 9.0.7 --- Oqtane.Maui/Oqtane.Maui.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 7f2170fd..0983aa45 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -67,11 +67,11 @@ - - - - - + + + + + From bb52402a173e42bc9cb77649d3dc6d5efad9340d Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Wed, 9 Jul 2025 12:09:00 +0200 Subject: [PATCH 26/56] feat: handle timezones and conversions with NodaTime --- .../OqtaneServiceCollectionExtensions.cs | 1 - .../Modules/Admin/Register/Index.razor | 3 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 3 +- .../Modules/Admin/UserProfile/Index.razor | 3 +- Oqtane.Client/Modules/Admin/Users/Add.razor | 3 +- Oqtane.Client/Modules/Admin/Users/Edit.razor | 3 +- Oqtane.Client/Modules/ModuleBase.cs | 40 ++---- .../Services/Interfaces/ITimeZoneService.cs | 18 --- Oqtane.Client/Services/TimeZoneService.cs | 22 ---- .../Controllers/TimeZoneController.cs | 29 ----- .../OqtaneServiceCollectionExtensions.cs | 1 - Oqtane.Shared/Oqtane.Shared.csproj | 1 + Oqtane.Shared/Shared/Utilities.cs | 123 +++++++++++++++++- 13 files changed, 142 insertions(+), 108 deletions(-) delete mode 100644 Oqtane.Client/Services/Interfaces/ITimeZoneService.cs delete mode 100644 Oqtane.Client/Services/TimeZoneService.cs delete mode 100644 Oqtane.Server/Controllers/TimeZoneController.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 9a89bce8..c5590d51 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -54,7 +54,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 712ba186..025c86c0 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -3,7 +3,6 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IUserService UserService -@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService @@ -115,7 +114,7 @@ { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezones = Utilities.GetTimeZones(); _timezoneid = PageState.Site.TimeZoneId; _initialized = true; } diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 172c4225..fcc9557f 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -10,7 +10,6 @@ @inject IAliasService AliasService @inject IThemeService ThemeService @inject ISettingService SettingService -@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @@ -508,7 +507,7 @@ Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (site != null) { - _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezones = Utilities.GetTimeZones(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 377b27d8..f993a7c1 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -9,7 +9,6 @@ @inject INotificationService NotificationService @inject IFileService FileService @inject IFolderService FolderService -@inject ITimeZoneService TimeZoneService @inject IJSRuntime jsRuntime @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @@ -404,7 +403,7 @@ _togglepassword = SharedLocalizer["ShowPassword"]; _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); - _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezones = Utilities.GetTimeZones(); if (PageState.User != null) { diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 510c87e7..2387e88e 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -5,7 +5,6 @@ @inject IUserService UserService @inject IProfileService ProfileService @inject ISettingService SettingService -@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -133,7 +132,7 @@ { try { - _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezones = Utilities.GetTimeZones(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _settings = new Dictionary(); _timezoneid = PageState.Site.TimeZoneId; diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index c6401a47..002e64df 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -6,7 +6,6 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject IFileService FileService -@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -204,7 +203,7 @@ _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _togglepassword = SharedLocalizer["ShowPassword"]; _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); - _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezones = Utilities.GetTimeZones(); if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) { diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index a066d159..59721817 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -507,24 +507,18 @@ namespace Oqtane.Modules if (datetime == null) return null; - TimeZoneInfo timezone = null; - try + string timezoneId = null; + + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) { - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) - { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); - } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) - { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); - } + timezoneId = PageState.User.TimeZoneId; } - catch + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) { - // The time zone ID was not found on the local computer + timezoneId = PageState.Site.TimeZoneId; } - return Utilities.UtcAsLocalDateTime(datetime, timezone); + return Utilities.UtcAsLocalDateTime(datetime, timezoneId); } public DateTime? LocalToUtc(DateTime? datetime) @@ -533,24 +527,18 @@ namespace Oqtane.Modules if (datetime == null) return null; - TimeZoneInfo timezone = null; - try + string timezoneId = null; + + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) { - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) - { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); - } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) - { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); - } + timezoneId = PageState.User.TimeZoneId; } - catch + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) { - // The time zone ID was not found on the local computer + timezoneId = PageState.Site.TimeZoneId; } - return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); + return Utilities.LocalDateAndTimeAsUtc(datetime, timezoneId); } // logging methods diff --git a/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs b/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs deleted file mode 100644 index c31f90b6..00000000 --- a/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Oqtane.Models; - -namespace Oqtane.Services -{ - /// - /// Service to store and retrieve entries - /// - public interface ITimeZoneService - { - /// - /// Get the list of time zones - /// - /// - Task> GetTimeZonesAsync(); - } -} diff --git a/Oqtane.Client/Services/TimeZoneService.cs b/Oqtane.Client/Services/TimeZoneService.cs deleted file mode 100644 index f7983b14..00000000 --- a/Oqtane.Client/Services/TimeZoneService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Oqtane.Documentation; -using Oqtane.Models; -using Oqtane.Shared; - -namespace Oqtane.Services -{ - [PrivateApi("Don't show in the documentation, as everything should use the Interface")] - public class TimeZoneService : ServiceBase, ITimeZoneService - { - public TimeZoneService(HttpClient http, SiteState siteState) : base(http, siteState) { } - - private string Apiurl => CreateApiUrl("TimeZone"); - - public async Task> GetTimeZonesAsync() - { - return await GetJsonAsync>($"{Apiurl}"); - } - } -} diff --git a/Oqtane.Server/Controllers/TimeZoneController.cs b/Oqtane.Server/Controllers/TimeZoneController.cs deleted file mode 100644 index 158d9f72..00000000 --- a/Oqtane.Server/Controllers/TimeZoneController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Oqtane.Models; -using Oqtane.Shared; - -namespace Oqtane.Controllers -{ - [Route(ControllerRoutes.ApiRoute)] - public class TimeZoneController : Controller - { - public TimeZoneController() {} - - // GET: api/ - [HttpGet] - public IEnumerable Get() - { - return TimeZoneInfo.GetSystemTimeZones() - .Select(item => new Models.TimeZone - { - Id = item.Id, - DisplayName = item.DisplayName - }) - .OrderBy(item => item.DisplayName); - } - } -} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 98a392cc..9b4b09e9 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -104,7 +104,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 77dc05d9..3f699bd6 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -22,6 +22,7 @@ + diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 95ae2cd1..bf2efe3e 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -1,4 +1,3 @@ -using Oqtane.Models; using System; using System.Collections.Generic; using System.Globalization; @@ -7,7 +6,14 @@ using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; + +using NodaTime; +using NodaTime.Extensions; + +using Oqtane.Models; + using File = Oqtane.Models.File; +using TimeZone = Oqtane.Models.TimeZone; namespace Oqtane.Shared { @@ -505,6 +511,7 @@ namespace Oqtane.Shared return $"[{@class.GetType()}] {message}"; } + //Time conversions with TimeZoneInfo public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string time, TimeZoneInfo localTimeZone = null) { if (date != null && !string.IsNullOrEmpty(time) && TimeSpan.TryParse(time, out TimeSpan timespan)) @@ -581,6 +588,120 @@ namespace Oqtane.Shared return (localDateTime?.Date, localTime); } + + //Time conversions with NodaTime (IANA) timezoneId + public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string time, string localTimeZoneId) + { + if (date != null && !string.IsNullOrEmpty(time) && TimeSpan.TryParse(time, out TimeSpan timespan)) + { + return LocalDateAndTimeAsUtc(date.Value.Date.Add(timespan), localTimeZoneId); + } + return null; + } + + public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, DateTime? time, string localTimeZoneId) + { + if (date != null) + { + if (time != null) + { + return LocalDateAndTimeAsUtc(date.Value.Date.Add(time.Value.TimeOfDay), localTimeZoneId); + } + return LocalDateAndTimeAsUtc(date.Value.Date, localTimeZoneId); + } + return null; + } + + public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string localTimeZoneId) + { + if (date != null) + { + DateTimeZone localTimeZone; + + if (!string.IsNullOrEmpty(localTimeZoneId)) + { + localTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(localTimeZoneId) ?? DateTimeZoneProviders.Tzdb.GetSystemDefault(); + } + else + { + localTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault(); + } + + var localDateTime = LocalDateTime.FromDateTime(date.Value); + return localTimeZone.AtLeniently(localDateTime).ToDateTimeUtc(); + } + return null; + } + + public static DateTime? UtcAsLocalDate(DateTime? dateTime, string timeZoneId) + { + return UtcAsLocalDateAndTime(dateTime, timeZoneId).date; + } + + public static DateTime? UtcAsLocalDateTime(DateTime? dateTime, string timeZoneId) + { + var result = UtcAsLocalDateAndTime(dateTime, timeZoneId); + if (result.date != null && !string.IsNullOrEmpty(result.time) && TimeSpan.TryParse(result.time, out TimeSpan timespan)) + { + result.date = result.date.Value.Add(timespan); + } + return result.date; + } + + public static (DateTime? date, string time) UtcAsLocalDateAndTime(DateTime? dateTime, string timeZoneId) + { + DateTimeZone localTimeZone; + + if (!string.IsNullOrEmpty(timeZoneId)) + { + localTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timeZoneId) ?? DateTimeZoneProviders.Tzdb.GetSystemDefault(); + } + else + { + localTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault(); + } + + DateTime? localDateTime = null; + string localTime = string.Empty; + + if (dateTime.HasValue && dateTime?.Kind != DateTimeKind.Local) + { + Instant instant; + + if (dateTime?.Kind == DateTimeKind.Unspecified) + { + // Treat Unspecified as Utc not Local. This is due to EF Core, on some databases, after retrieval will have DateTimeKind as Unspecified. + // All values in database should be UTC. + // Normal .net conversion treats Unspecified as local. + // https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.converttime?view=net-6.0 + instant = Instant.FromDateTimeUtc(new DateTime(dateTime.Value.Ticks, DateTimeKind.Utc)); + } + else + { + instant = Instant.FromDateTimeUtc(dateTime.Value); + } + + localDateTime = instant.InZone(localTimeZone).ToDateTimeOffset().DateTime; + } + + if (localDateTime != null && localDateTime.Value.TimeOfDay.TotalSeconds != 0) + { + localTime = localDateTime.Value.ToString("HH:mm"); + } + + return (localDateTime?.Date, localTime); + } + + public static List GetTimeZones() + { + return [.. DateTimeZoneProviders.Tzdb.GetAllZones() + .Select(tz => new TimeZone() + { + Id = tz.Id, + DisplayName = tz.ToString() + })]; + } + public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate) { DateTime currentUtcTime = DateTime.UtcNow; From e9035df9d20022b9471fa085a518f67eaa36f852 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 14 Jul 2025 13:40:52 -0700 Subject: [PATCH 27/56] Update Package Dependencies --- Oqtane.Maui/Oqtane.Maui.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 0983aa45..8e683de5 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -72,9 +72,9 @@ - - - + + + From d2ff49fe73df0f0872c166938e324f74e98b20ad Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 14 Jul 2025 16:10:50 -0700 Subject: [PATCH 28/56] [FIX] #5164 - Set z-index for .dropdown-menu in .app-moduleactions --- Oqtane.Maui/wwwroot/css/app.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css index 538e3a4c..fc0f07d0 100644 --- a/Oqtane.Maui/wwwroot/css/app.css +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -75,6 +75,10 @@ app { color: gray; } +.app-moduleactions .dropdown-menu { + z-index: 9000; +} + .app-moduleactions .dropdown-submenu { position: relative; } @@ -270,4 +274,4 @@ app { .app-logo .navbar-brand { padding: 5px 20px 5px 20px; -} \ No newline at end of file +} From 5a24f872935f07316f479f593360cb7de6be5c6c Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 14 Jul 2025 17:07:39 -0700 Subject: [PATCH 29/56] =?UTF-8?q?[FIX]=20#5164=C2=A0=E2=80=91=20Set=20z?= =?UTF-8?q?=E2=80=91index=20for=20.dropdown=E2=80=91menu=20in=20.app?= =?UTF-8?q?=E2=80=91moduleactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Oqtane.Server/wwwroot/css/app.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index 0b13a837..c7eec5f5 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -75,6 +75,10 @@ app { color: gray; } +.app-moduleactions .dropdown-menu { + z-index: 9000; +} + .app-moduleactions .dropdown-submenu { position: relative; } @@ -281,4 +285,4 @@ app { .gdpr-consent-bar .btn-hide{ top: 0; right: 5px; -} \ No newline at end of file +} From 9690f1df484f189c67795c117b7c27ea629b2ddc Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 14 Jul 2025 17:09:24 -0700 Subject: [PATCH 30/56] =?UTF-8?q?[FIX]=20oqtane#5164=20=E2=80=93=20Raise?= =?UTF-8?q?=20z=E2=80=91index=20for=20.app=E2=80=91moduleactions=20.dropdo?= =?UTF-8?q?wn=E2=80=91menu=20to=209999?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Oqtane.Server/wwwroot/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index c7eec5f5..00461f64 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -76,7 +76,7 @@ app { } .app-moduleactions .dropdown-menu { - z-index: 9000; + z-index: 9999; } .app-moduleactions .dropdown-submenu { From 948fab50ee944d240d178dc50055572ad1432f77 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 14 Jul 2025 17:10:07 -0700 Subject: [PATCH 31/56] =?UTF-8?q?[FIX]=20#5164=20=E2=80=93=20Raise=20z?= =?UTF-8?q?=E2=80=91index=20for=20.app=E2=80=91moduleactions=20.dropdown?= =?UTF-8?q?=E2=80=91menu=20to=209999?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Oqtane.Maui/wwwroot/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css index fc0f07d0..ab9c6adb 100644 --- a/Oqtane.Maui/wwwroot/css/app.css +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -76,7 +76,7 @@ app { } .app-moduleactions .dropdown-menu { - z-index: 9000; + z-index: 9999; } .app-moduleactions .dropdown-submenu { From 0be7f1bdb5c9f174ce9634cc647bcfcedf9778a2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 21 Jul 2025 09:14:07 -0400 Subject: [PATCH 32/56] add new option to FileManager component to anonymize filenames during upload --- .../Modules/Controls/FileManager.razor | 5 ++- Oqtane.Client/UI/Interop.cs | 6 ++-- Oqtane.Server/Controllers/FileController.cs | 36 ++++++++++--------- Oqtane.Server/wwwroot/js/interop.js | 10 ++++-- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 1e5fa1c5..9581c58a 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -157,6 +157,9 @@ [Parameter] public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false + [Parameter] + public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - default false + [Parameter] public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB @@ -408,7 +411,7 @@ } // upload files - var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token); + var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token); // reset progress indicators if (ShowProgress) diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 8d547da7..3a56783f 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -224,17 +224,17 @@ namespace Oqtane.UI public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) { - UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1); + UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1, false); return Task.CompletedTask; } - public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default) + public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, bool anonymizeuploadfilenames, CancellationToken cancellationToken = default) { try { return _jsRuntime.InvokeAsync( "Oqtane.Interop.uploadFiles", cancellationToken, - posturl, folder, id, antiforgerytoken, jwt, chunksize); + posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames); } catch { diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index f0b72f22..3db7ecae 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -444,9 +444,14 @@ namespace Oqtane.Controllers } // ensure filename is valid - if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) + string fileName = formfile.FileName; + if (Path.GetExtension(fileName).Contains(':')) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); + fileName = fileName.Substring(0, fileName.LastIndexOf(':')); // remove invalid suffix from extension + } + if (!fileName.IsPathOrFileValid() || !HasValidFileExtension(fileName)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", fileName); return StatusCode((int)HttpStatusCode.Forbidden); } @@ -458,8 +463,8 @@ namespace Oqtane.Controllers return StatusCode((int)HttpStatusCode.Forbidden); } - // create file name using header values - string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); + // create file name using header part values + fileName += ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); string folderPath = ""; try @@ -532,13 +537,13 @@ namespace Oqtane.Controllers string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999" int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1)); - filename = Path.GetFileNameWithoutExtension(filename); // base filename + filename = Path.GetFileNameWithoutExtension(filename); // base filename including original file extension string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts // if all of the file parts exist (note that file parts can arrive out of order) if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) { - // merge file parts into temp file (in case another user is trying to get the file) + // merge file parts into temp file (in case another user is trying to read the file) bool success = true; using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) { @@ -559,25 +564,22 @@ namespace Oqtane.Controllers } // clean up file parts - foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) + foreach (var file in fileparts) { - if (fileparts.Contains(file)) + try { - try - { - System.IO.File.Delete(file); - } - catch - { - // unable to delete part - ignore - } + System.IO.File.Delete(file); + } + catch + { + // unable to delete part - ignore } } // rename temp file if (success) { - // remove file if it already exists (as well as any thumbnails which may exist) + // remove existing file (as well as any thumbnails) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) { if (Path.GetExtension(file) != ".tmp") diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 191d9823..fecc4c99 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -311,7 +311,7 @@ Oqtane.Interop = { } return files; }, - uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { + uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) { var success = true; var fileinput = document.getElementById('FileInput_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id); @@ -344,16 +344,22 @@ Oqtane.Interop = { const totalParts = Math.ceil(file.size / chunkSize); let partCount = 0; + let filename = file.name; + if (anonymizeuploadfilenames) { + filename = crypto.randomUUID() + '.' + filename.split('.').pop(); + } + const uploadPart = () => { const start = partCount * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); return new Promise((resolve, reject) => { + let formdata = new FormData(); formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('folder', folder); - formdata.append('formfile', chunk, file.name); + formdata.append('formfile', chunk, filename); var credentials = 'same-origin'; var headers = new Headers(); From a981dd0e978622ac12c1747071e0ec2786c83082 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 21 Jul 2025 16:34:34 -0400 Subject: [PATCH 33/56] fix Control Panel to initialize extended module permissions when module is added or copied --- .../Theme/ControlPanelInteractive.razor | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 5dd940f8..a42a1f12 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -353,7 +353,7 @@ module.PageId = PageState.Page.PageId; module.ModuleDefinitionName = _moduleDefinitionName; module.AllPages = false; - module.PermissionList = GenerateDefaultPermissions(module.SiteId); + module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName); module = await ModuleService.AddModuleAsync(module); newModuleId = module.ModuleId; @@ -365,7 +365,7 @@ module.SiteId = PageState.Page.SiteId; module.PageId = PageState.Page.PageId; module.AllPages = false; - module.PermissionList = GenerateDefaultPermissions(module.SiteId); + module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName); module = await ModuleService.AddModuleAsync(module); var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId); @@ -430,7 +430,7 @@ } } - private List GenerateDefaultPermissions(int siteId) + private List GenerateDefaultPermissions(int siteId, string moduleDefinitionName) { var permissions = new List(); if (_visibility == "view") @@ -443,8 +443,22 @@ // set module view permissions to page edit permissions permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit); } - // set module edit permissions to page edit permissions - permissions = SetPermissions(permissions, siteId, PermissionNames.Edit, PermissionNames.Edit); + + // get module permissions + var permissionNames = $"{PermissionNames.View},{PermissionNames.Edit}"; + var moduleDefinition = _allModuleDefinitions.FirstOrDefault(item => item.ModuleDefinitionName == moduleDefinitionName); + if (moduleDefinition != null && !string.IsNullOrEmpty(moduleDefinition.PermissionNames)) + { + permissionNames = moduleDefinition.PermissionNames; + } + foreach (var permission in permissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (permission != PermissionNames.View) + { + // set remaining module permissions to page edit permissions + permissions = SetPermissions(permissions, siteId, permission, PermissionNames.Edit); + } + } return permissions; } From 372db9dcfa57f7ef3ebd22016aa70f303811fcd7 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 22 Jul 2025 07:45:03 +0200 Subject: [PATCH 34/56] Solutions References update MySql.Data 9.4.0 HtmlAgilityPack 1.12.2 --- Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj | 2 +- Oqtane.Server/Oqtane.Server.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index beedbc8e..86ccefef 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 139dcc96..44bab0f5 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -47,7 +47,7 @@ - + From 262fa6b99bf11d6e0c0e63cb14fe1897f6db26ce Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 22 Jul 2025 09:23:26 -0400 Subject: [PATCH 35/56] improve documentation --- .../Controls/Theme/ControlPanelInteractive.razor | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index a42a1f12..10ce97ed 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -433,6 +433,8 @@ private List GenerateDefaultPermissions(int siteId, string moduleDefinitionName) { var permissions = new List(); + + // set module view permissions if (_visibility == "view") { // set module view permissions to page view permissions @@ -444,18 +446,18 @@ permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit); } - // get module permissions - var permissionNames = $"{PermissionNames.View},{PermissionNames.Edit}"; + // set remaining module permissions + var permissionNames = PermissionNames.Edit; var moduleDefinition = _allModuleDefinitions.FirstOrDefault(item => item.ModuleDefinitionName == moduleDefinitionName); if (moduleDefinition != null && !string.IsNullOrEmpty(moduleDefinition.PermissionNames)) { - permissionNames = moduleDefinition.PermissionNames; + permissionNames = moduleDefinition.PermissionNames; // custom module permissions } foreach (var permission in permissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { if (permission != PermissionNames.View) { - // set remaining module permissions to page edit permissions + // set module permissions to page edit permissions permissions = SetPermissions(permissions, siteId, permission, PermissionNames.Edit); } } From 83ba9ca73e7d78c32dd0743f40eda0528888c29c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 22 Jul 2025 16:07:52 -0400 Subject: [PATCH 36/56] improve user experience of permissions grid --- .../Modules/Controls/PermissionGrid.razor | 477 +++++++++--------- .../Modules/Controls/TriStateCheckBox.razor | 38 +- .../Modules/Controls/TriStateCheckBox.resx | 3 + Oqtane.Server/wwwroot/images/disabled.png | Bin 0 -> 875 bytes 4 files changed, 263 insertions(+), 255 deletions(-) create mode 100644 Oqtane.Server/wwwroot/images/disabled.png diff --git a/Oqtane.Client/Modules/Controls/PermissionGrid.razor b/Oqtane.Client/Modules/Controls/PermissionGrid.razor index a1f6e094..2a883920 100644 --- a/Oqtane.Client/Modules/Controls/PermissionGrid.razor +++ b/Oqtane.Client/Modules/Controls/PermissionGrid.razor @@ -9,62 +9,26 @@ @if (_permissions != null) { -
-
-
- - - - - @foreach (var permissionname in _permissionnames) - { - - } - - @foreach (Role role in _roles) - { +
+
+
+
@Localizer["Role"]@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "
"))
+ - + @foreach (var permissionname in _permissionnames) { - + } - } - -
@role.Name@Localizer["Role"] - - @((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "
"))
-
-
-
-
-
- @if (_users.Count != 0) - { -
-
-
-
- - - - - @foreach (var permissionname in _permissionnames) - { - - } - - - - @foreach (User user in _users) + @foreach (Role role in _roles) { - + @foreach (var permissionname in _permissionnames) { - } @@ -72,200 +36,242 @@
@Localizer["User"]@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "
"))
@user.DisplayName (@user.Username)@role.Name - + +

- } +
+
+
+
+ @if (_users.Count != 0) + { +
+
+
+
+ + + + + @foreach (var permissionname in _permissionnames) + { + + } + + + + @foreach (User user in _users) + { + + + @foreach (var permissionname in _permissionnames) + { + + } + + } + +
@Localizer["User"]@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "
"))
@user.DisplayName (@user.Username) + +
+
+ } +
+
+
+
+ +
+
+ +
+
+
+
+ +
-
-
- -
-
- -
-
-
-
- -
-
- } @code { - private List _permissionnames; - private List _permissions; - private List _roles; - private List _users = new List(); - private AutoComplete _user; - private string _message = string.Empty; + private List _permissionnames; + private List _permissions; + private List _roles; + private List _users = new List(); + private AutoComplete _user; + private string _message = string.Empty; - [Parameter] - public string EntityName { get; set; } + [Parameter] + public string EntityName { get; set; } - [Parameter] - public string PermissionNames { get; set; } + [Parameter] + public string PermissionNames { get; set; } - [Parameter] - public string Permissions { get; set; } // deprecated - use PermissionList instead + [Parameter] + public string Permissions { get; set; } // deprecated - use PermissionList instead - [Parameter] - public List PermissionList { get; set; } + [Parameter] + public List PermissionList { get; set; } protected override async Task OnInitializedAsync() - { - if (!string.IsNullOrEmpty(Permissions)) - { - PermissionList = JsonSerializer.Deserialize>(Permissions); - } + { + if (!string.IsNullOrEmpty(Permissions)) + { + PermissionList = JsonSerializer.Deserialize>(Permissions); + } - _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true); - if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { - _roles.RemoveAll(item => item.Name == RoleNames.Host); - } + _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true); + _roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role - // get permission names - if (string.IsNullOrEmpty(PermissionNames)) - { - _permissionnames = new List(); - _permissionnames.Add(Shared.PermissionNames.View); - _permissionnames.Add(Shared.PermissionNames.Edit); - } - else - { - _permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); - } + // get permission names + if (string.IsNullOrEmpty(PermissionNames)) + { + _permissionnames = new List(); + _permissionnames.Add(Shared.PermissionNames.View); + _permissionnames.Add(Shared.PermissionNames.Edit); + } + else + { + _permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); + } - // initialize permissions - _permissions = new List(); - if (PermissionList != null && PermissionList.Any()) - { - foreach (var permission in PermissionList) - { - _permissions.Add(permission); - if (permission.UserId != null) - { - if (!_users.Any(item => item.UserId == permission.UserId.Value)) - { - _users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId)); - } - } - } - } - else - { - foreach (string permissionname in _permissionnames) - { - // permission names can be in the form of "EntityName:PermissionName:Roles" - if (permissionname.Contains(":")) - { - var segments = permissionname.Split(':'); - if (segments.Length == 3) - { - foreach (var role in segments[2].Split(';')) - { - _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true)); - } - // ensure admin access - if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin)) - { - _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true)); - } - } - } - else - { - _permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true)); - } - } - } - } + // initialize permissions + _permissions = new List(); + if (PermissionList != null && PermissionList.Any()) + { + foreach (var permission in PermissionList) + { + _permissions.Add(permission); + if (permission.UserId != null) + { + if (!_users.Any(item => item.UserId == permission.UserId.Value)) + { + _users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId)); + } + } + } + } + else + { + foreach (string permissionname in _permissionnames) + { + // permission names can be in the form of "EntityName:PermissionName:Roles" + if (permissionname.Contains(":")) + { + var segments = permissionname.Split(':'); + if (segments.Length == 3) + { + foreach (var role in segments[2].Split(';')) + { + _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true)); + } + // ensure admin access + if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin)) + { + _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true)); + } + } + } + else + { + _permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true)); + } + } + } + } - private string GetPermissionName(string permissionName) - { - return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName; - } + private string GetPermissionName(string permissionName) + { + return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName; + } - private string GetEntityName(string permissionName) - { - return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName; - } + private string GetEntityName(string permissionName) + { + return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName; + } - private string DisplayPermissionName(string permissionName) - { - var name = Localizer[GetPermissionName(permissionName)].ToString(); - name += " " + Localizer[GetEntityName(permissionName)].ToString(); - return name; - } + private string DisplayPermissionName(string permissionName) + { + var name = Localizer[GetPermissionName(permissionName)].ToString(); + name += " " + Localizer[GetEntityName(permissionName)].ToString(); + return name; + } - private bool? GetPermissionValue(string permissionName, string roleName, int userId) - { - bool? isauthorized = null; - if (roleName != "") - { - var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); - if (permission != null) - { - isauthorized = permission.IsAuthorized; - } - } - else - { - var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); - if (permission != null) - { - isauthorized = permission.IsAuthorized; - } - } - return isauthorized; - } + private bool? GetPermissionValue(string permissionName, string roleName, int userId) + { + bool? isauthorized = null; + if (roleName != "") + { + var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); + if (permission != null) + { + isauthorized = permission.IsAuthorized; + } + } + else + { + var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); + if (permission != null) + { + isauthorized = permission.IsAuthorized; + } + } + return isauthorized; + } - private bool GetPermissionDisabled(string permissionName, string roleName) - { - if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { - return true; - } - else - { - if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) - { - return true; - } - else - { - return false; - } - } - } + private bool GetPermissionDisabled(string permissionName, string roleName) + { + var disabled = false; - private void PermissionChanged(bool? value, string permissionName, string roleName, int userId) - { - if (roleName != "") - { - var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); - if (permission != null) - { - _permissions.Remove(permission); - } - if (value != null) - { - _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value)); - } - } - else - { - var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); - if (permission != null) - { - _permissions.Remove(permission); - } - if (value != null) - { - _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value)); - } - } - } + // administrator role permissions can only be changed by a host + if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + disabled = true; + } + + // API permissions can only be changed by an administrator + if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) + { + disabled = true; + } + + return disabled; + } + + private bool? PermissionChanged(bool? value, string permissionName, string roleName, int userId) + { + if (roleName != "") + { + var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); + if (permission != null) + { + _permissions.Remove(permission); + } + + // system roles cannot be denied - only custom roles can be denied + var role = _roles.FirstOrDefault(item => item.Name == roleName); + if (value != null && !value.Value && role.IsSystem) + { + value = null; + } + + if (value != null) + { + _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value)); + } + } + else + { + var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); + if (permission != null) + { + _permissions.Remove(permission); + } + if (value != null) + { + _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value)); + } + } + return value; + } private async Task> GetUsers(string filter) { @@ -305,29 +311,20 @@ private void ValidatePermissions() { - // remove deny all users, unauthenticated, and registered users - var permissions = _permissions.Where(item => !item.IsAuthorized && - (item.RoleName == RoleNames.Everyone || item.RoleName == RoleNames.Unauthenticated || item.RoleName == RoleNames.Registered)).ToList(); - foreach (var permission in permissions) - { - _permissions.Remove(permission); - } if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - // remove deny administrators and host users - permissions = _permissions.Where(item => !item.IsAuthorized && - (item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList(); - foreach (var permission in permissions) + // remove host role permissions + var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList(); + foreach (var permission in permissions) + { + _permissions.Remove(permission); + } + // add host role permissions if administrator role is not assigned (to prevent lockout) + foreach (var permissionname in _permissionnames) { - _permissions.Remove(permission); - } - foreach (var permissionname in _permissionnames) - { - // add administrators role if neither host or administrator is assigned - if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && - (item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host))) + if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin)) { - _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Admin, null, true)); + _permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Host, null, true)); } } } diff --git a/Oqtane.Client/Modules/Controls/TriStateCheckBox.razor b/Oqtane.Client/Modules/Controls/TriStateCheckBox.razor index 538aa3ae..e87c2874 100644 --- a/Oqtane.Client/Modules/Controls/TriStateCheckBox.razor +++ b/Oqtane.Client/Modules/Controls/TriStateCheckBox.razor @@ -16,7 +16,7 @@ public bool Disabled { get; set; } [Parameter] - public Action OnChange { get; set; } + public Func OnChange { get; set; } protected override void OnInitialized() { @@ -41,27 +41,35 @@ break; } + _value = OnChange(_value); SetImage(); - OnChange(_value); } } private void SetImage() { - switch (_value) + if (!Disabled) { - case true: - _src = "images/checked.png"; - _title = Localizer["PermissionGranted"]; - break; - case false: - _src = "images/unchecked.png"; - _title = Localizer["PermissionDenied"]; - break; - case null: - _src = "images/null.png"; - _title = string.Empty; - break; + switch (_value) + { + case true: + _src = "images/checked.png"; + _title = Localizer["PermissionGranted"]; + break; + case false: + _src = "images/unchecked.png"; + _title = Localizer["PermissionDenied"]; + break; + case null: + _src = "images/null.png"; + _title = string.Empty; + break; + } + } + else + { + _src = "images/disabled.png"; + _title = Localizer["PermissionDisabled"]; } StateHasChanged(); diff --git a/Oqtane.Client/Resources/Modules/Controls/TriStateCheckBox.resx b/Oqtane.Client/Resources/Modules/Controls/TriStateCheckBox.resx index 3def68ad..af745b24 100644 --- a/Oqtane.Client/Resources/Modules/Controls/TriStateCheckBox.resx +++ b/Oqtane.Client/Resources/Modules/Controls/TriStateCheckBox.resx @@ -123,4 +123,7 @@ Permission Denied + + Permission Disabled + \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/images/disabled.png b/Oqtane.Server/wwwroot/images/disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..cc58ba2ae0ec9fb4ffbfe574f7b6575c5692ef62 GIT binary patch literal 875 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBufiR<}hF1enP@=>&q9iy!t)x7$D3u`~F*C13&(AeP!Bo#s z&q6x$)e4}RZK)BSX`Y^13>-iXD}xjxD+42tpt($7 zl|hykKo(S&p@9KN_Dr5wjaTps8K62TPZ!4!3;)(&-)s>_fwiyh%`7#)>C&N;l5FNC z;xeT{?oWcF;Q6>e+>6W=*;yS0Y<}>I| zqwws@)0Gt$4nDj6sz*!rpvakDJePCRFp>lnPvLrPP( zXr4Q|aq)>);#^yNFE22?!??IZMApkf^u}C+wV&Hq;DRjz}khkU+KR7No|F81a;pufopGu~qzaLgzRDNK&fp6aV>zfmJZ${i Date: Wed, 23 Jul 2025 14:52:18 -0400 Subject: [PATCH 37/56] localize time zone names --- .../OqtaneServiceCollectionExtensions.cs | 1 + .../Modules/Admin/UserProfile/Index.razor | 5 +- .../Resources/TimeZoneResources.resx | 129 ++++++++++++++++++ .../Services/Interfaces/ITimeZoneService.cs | 17 +++ Oqtane.Client/Services/TimeZoneService.cs | 34 +++++ Oqtane.Client/TimeZoneResources.cs | 14 ++ .../OqtaneServiceCollectionExtensions.cs | 1 + Oqtane.Shared/Shared/Utilities.cs | 10 +- 8 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 Oqtane.Client/Resources/TimeZoneResources.resx create mode 100644 Oqtane.Client/Services/Interfaces/ITimeZoneService.cs create mode 100644 Oqtane.Client/Services/TimeZoneService.cs create mode 100644 Oqtane.Client/TimeZoneResources.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index c5590d51..eae6ba0f 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); // providers diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index f993a7c1..95ecbd52 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -9,6 +9,7 @@ @inject INotificationService NotificationService @inject IFileService FileService @inject IFolderService FolderService +@inject ITimeZoneService TimeZoneService @inject IJSRuntime jsRuntime @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @@ -366,7 +367,6 @@ } @code { - private List _timezones; private bool _initialized = false; private string _passwordrequirements; private string _username = string.Empty; @@ -380,6 +380,7 @@ private string _displayname = string.Empty; private FileManager _filemanager; private int _folderid = -1; + private List _timezones; private string _timezoneid = string.Empty; private int _photofileid = -1; private File _photo = null; @@ -403,7 +404,7 @@ _togglepassword = SharedLocalizer["ShowPassword"]; _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); if (PageState.User != null) { diff --git a/Oqtane.Client/Resources/TimeZoneResources.resx b/Oqtane.Client/Resources/TimeZoneResources.resx new file mode 100644 index 00000000..d736dcea --- /dev/null +++ b/Oqtane.Client/Resources/TimeZoneResources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + (UTC) Coordinated Universal Time + + + (UTC-05:00) Eastern Time (US & Canada) + + + (UTC-08:00) Pacific Time (US & Canada) + + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs b/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs new file mode 100644 index 00000000..de134838 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/ITimeZoneService.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Oqtane.Models; + +namespace Oqtane.Services +{ + /// + /// Service to retrieve entries + /// + public interface ITimeZoneService + { + /// + /// Get the list of time zones + /// + /// + List GetTimeZones(); + } +} diff --git a/Oqtane.Client/Services/TimeZoneService.cs b/Oqtane.Client/Services/TimeZoneService.cs new file mode 100644 index 00000000..f6cab35b --- /dev/null +++ b/Oqtane.Client/Services/TimeZoneService.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Localization; +using Oqtane.Documentation; +using Oqtane.Models; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class TimeZoneService : ITimeZoneService + { + private readonly IStringLocalizer _TimeZoneLocalizer; + + public TimeZoneService(IStringLocalizer TimeZoneLocalizer) + { + _TimeZoneLocalizer = TimeZoneLocalizer; + } + + public List GetTimeZones() + { + var _timezones = new List(); + foreach (var timezone in Utilities.GetTimeZones()) + { + _timezones.Add(new TimeZone + { + Id = timezone.Id, + DisplayName = _TimeZoneLocalizer[timezone.Id] + }); + } + return _timezones.OrderBy(item => item.DisplayName).ToList(); + } + } +} diff --git a/Oqtane.Client/TimeZoneResources.cs b/Oqtane.Client/TimeZoneResources.cs new file mode 100644 index 00000000..cd0125fd --- /dev/null +++ b/Oqtane.Client/TimeZoneResources.cs @@ -0,0 +1,14 @@ +namespace Oqtane +{ + /// + /// Dummy class used to collect shared resource strings for this application + /// + /// + /// This class is mostly used with IStringLocalizer and IHtmlLocalizer interfaces. + /// The class must reside at the project root. + /// + public class TimeZoneResources + { + + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 9b4b09e9..98a392cc 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -104,6 +104,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index bf2efe3e..f5faff87 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -695,11 +695,11 @@ namespace Oqtane.Shared public static List GetTimeZones() { return [.. DateTimeZoneProviders.Tzdb.GetAllZones() - .Select(tz => new TimeZone() - { - Id = tz.Id, - DisplayName = tz.ToString() - })]; + .Select(tz => new TimeZone() + { + Id = tz.Id, + DisplayName = tz.Id + })]; } public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate) From 97116b4e0cf5938bb0bc0f114e6f54513bb49733 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 23 Jul 2025 16:40:12 -0400 Subject: [PATCH 38/56] fix #5410 - allow duplicate email addresses --- .../Tenant/06010401_RemoveUniqueEmailIndex.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Oqtane.Server/Migrations/Tenant/06010401_RemoveUniqueEmailIndex.cs diff --git a/Oqtane.Server/Migrations/Tenant/06010401_RemoveUniqueEmailIndex.cs b/Oqtane.Server/Migrations/Tenant/06010401_RemoveUniqueEmailIndex.cs new file mode 100644 index 00000000..ac783753 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/06010401_RemoveUniqueEmailIndex.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.06.01.04.01")] + public class RemoveUniqueEmailIndex : MultiDatabaseMigration + { + public RemoveUniqueEmailIndex(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + // framework uses RequireUniqueEmail = False in .NET Identity configuration + var aspNetUsersEntityBuilder = new AspNetUsersEntityBuilder(migrationBuilder, ActiveDatabase); + aspNetUsersEntityBuilder.DropIndex("EmailIndex"); + aspNetUsersEntityBuilder.AddIndex("EmailIndex", "NormalizedEmail", false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} From b0dee4a60c2a5662720b1586562b36738bb9463f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 25 Jul 2025 15:22:26 -0400 Subject: [PATCH 39/56] fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations --- Oqtane.Database.MySQL/MySQLDatabase.cs | 8 ++--- .../PostgreSQLDatabase.cs | 4 +-- .../SqlServerDatabase.cs | 5 +++ Oqtane.Database.Sqlite/SqliteDatabase.cs | 5 +++ Oqtane.Server/Databases/DatabaseBase.cs | 4 +-- .../Databases/Interfaces/IDatabase.cs | 4 +-- .../Infrastructure/DatabaseManager.cs | 2 +- .../EntityBuilders/BaseEntityBuilder.cs | 24 +++++++------- .../Framework/MultiDatabaseMigration.cs | 13 +++++--- ...000101_AddAdditionColumnToNotifications.cs | 2 +- .../Tenant/02000101_UpdateIconColumnInPage.cs | 4 +-- ...02_UpdateDefaultContainerTypeInSitePage.cs | 12 +++---- .../02000203_DropDefaultLayoutInSite.cs | 12 +++---- .../Tenant/02030001_AddFolderCapacity.cs | 2 +- .../Tenant/03000201_UpdateSettingIsPublic.cs | 4 +-- .../Tenant/03000202_UpdateSettingIsPrivate.cs | 2 +- .../Tenant/06010402_ResetTimeZone.cs | 32 +++++++++++++++++++ 17 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 Oqtane.Server/Migrations/Tenant/06010402_ResetTimeZone.cs diff --git a/Oqtane.Database.MySQL/MySQLDatabase.cs b/Oqtane.Database.MySQL/MySQLDatabase.cs index a3b8d307..2f50c1d4 100644 --- a/Oqtane.Database.MySQL/MySQLDatabase.cs +++ b/Oqtane.Database.MySQL/MySQLDatabase.cs @@ -75,13 +75,9 @@ namespace Oqtane.Database.MySQL return dr; } - public override string RewriteName(string name, bool isQuery) + public override string DelimitName(string name) { - if (name.ToLower() == "rows" && isQuery) - { - name = $"`{name}`"; // escape reserved word in SQL query - } - return name; + return $"`{name}`"; } public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) diff --git a/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs b/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs index a9e6e0d8..7e20a86d 100644 --- a/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs +++ b/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs @@ -87,9 +87,9 @@ namespace Oqtane.Database.PostgreSQL return _rewriter.RewriteName(name); } - public override string RewriteName(string name, bool isQuery) + public override string DelimitName(string name) { - return _rewriter.RewriteName(name); + return $"\"{name}\""; } public override string RewriteValue(string value, string type) diff --git a/Oqtane.Database.SqlServer/SqlServerDatabase.cs b/Oqtane.Database.SqlServer/SqlServerDatabase.cs index a7d8f641..7f52b8be 100644 --- a/Oqtane.Database.SqlServer/SqlServerDatabase.cs +++ b/Oqtane.Database.SqlServer/SqlServerDatabase.cs @@ -46,6 +46,11 @@ namespace Oqtane.Database.SqlServer } } + public override string DelimitName(string name) + { + return $"[{name}]"; + } + public override int ExecuteNonQuery(string connectionString, string query) { var conn = new SqlConnection(FormatConnectionString(connectionString)); diff --git a/Oqtane.Database.Sqlite/SqliteDatabase.cs b/Oqtane.Database.Sqlite/SqliteDatabase.cs index 39c6cfd3..df132079 100644 --- a/Oqtane.Database.Sqlite/SqliteDatabase.cs +++ b/Oqtane.Database.Sqlite/SqliteDatabase.cs @@ -84,6 +84,11 @@ namespace Oqtane.Database.Sqlite return dr; } + public override string DelimitName(string name) + { + return $"\"{name}\""; + } + public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) { return optionsBuilder.UseSqlite(connectionString) diff --git a/Oqtane.Server/Databases/DatabaseBase.cs b/Oqtane.Server/Databases/DatabaseBase.cs index 2342874f..668fdd38 100644 --- a/Oqtane.Server/Databases/DatabaseBase.cs +++ b/Oqtane.Server/Databases/DatabaseBase.cs @@ -61,12 +61,12 @@ namespace Oqtane.Databases public abstract IDataReader ExecuteReader(string connectionString, string query); - public virtual string RewriteName(string name) + public virtual string DelimitName(string name) { return name; } - public virtual string RewriteName(string name, bool isQuery) + public virtual string RewriteName(string name) { return name; } diff --git a/Oqtane.Server/Databases/Interfaces/IDatabase.cs b/Oqtane.Server/Databases/Interfaces/IDatabase.cs index 3c68cf03..8b9e7586 100644 --- a/Oqtane.Server/Databases/Interfaces/IDatabase.cs +++ b/Oqtane.Server/Databases/Interfaces/IDatabase.cs @@ -26,9 +26,9 @@ namespace Oqtane.Databases.Interfaces public IDataReader ExecuteReader(string connectionString, string query); - public string RewriteName(string name); + public string DelimitName(string name); // only used in conjunction with method using MigrationBuilder.Sql() - public string RewriteName(string name, bool isQuery); + public string RewriteName(string name); public string RewriteValue(string value, string type); diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 1ee2a138..ff06f408 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -738,7 +738,7 @@ namespace Oqtane.Infrastructure databases += "{ \"Name\": \"LocalDB\", \"ControlType\": \"Oqtane.Installer.Controls.LocalDBConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer\" },"; databases += "{ \"Name\": \"SQL Server\", \"ControlType\": \"Oqtane.Installer.Controls.SqlServerConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer\" },"; databases += "{ \"Name\": \"SQLite\", \"ControlType\": \"Oqtane.Installer.Controls.SqliteConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.Sqlite.SqliteDatabase, Oqtane.Database.Sqlite\" },"; - databases += "{ \"Name\": \"MySQL\", \"ControlType\": \"Oqtane.Installer.Controls.MySQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.MySQL.SqlServerDatabase, Oqtane.Database.MySQL\" },"; + databases += "{ \"Name\": \"MySQL\", \"ControlType\": \"Oqtane.Installer.Controls.MySQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.MySQL.MySQLDatabase, Oqtane.Database.MySQL\" },"; databases += "{ \"Name\": \"PostgreSQL\", \"ControlType\": \"Oqtane.Installer.Controls.PostgreSQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.PostgreSQL.PostgreSQLDatabase, Oqtane.Database.PostgreSQL\" }"; databases += "]"; _configManager.AddOrUpdateSetting(SettingKeys.AvailableDatabasesSection, databases, true); diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index 7202921e..430a72c6 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -33,28 +33,28 @@ namespace Oqtane.Migrations.EntityBuilders protected string Schema { get; init; } - private string RewriteSqlEntityTableName(string name) + private string AddSchema(string name) { - if (Schema == null) + if (string.IsNullOrEmpty(Schema)) { - return RewriteName(name); + return name; } else { - return $"{Schema}.{RewriteName(name)}"; + return $"{Schema}.{name}"; } } + private string DelimitName(string name) + { + return ActiveDatabase.DelimitName(name); + } + private string RewriteName(string name) { return ActiveDatabase.RewriteName(name); } - private string RewriteName(string name, bool isQuery) - { - return ActiveDatabase.RewriteName(name, isQuery); - } - private string RewriteValue(string value, string type) { return ActiveDatabase.RewriteValue(value, type); @@ -468,9 +468,10 @@ namespace Oqtane.Migrations.EntityBuilders public void DeleteFromTable(string condition = "") { - var deleteSql = $"DELETE FROM {RewriteSqlEntityTableName(EntityTableName)} "; + var deleteSql = $"DELETE FROM {AddSchema(DelimitName(RewriteName(EntityTableName)))} "; if(!string.IsNullOrEmpty(condition)) { + // note that condition values must be created using RewriteName(), DelimitName(), RewriteValue() if targeting multiple database platforms deleteSql += $"WHERE {condition}"; } _migrationBuilder.Sql(deleteSql); @@ -488,9 +489,10 @@ namespace Oqtane.Migrations.EntityBuilders public void UpdateColumn(string columnName, string value, string type, string condition) { - var updateSql = $"UPDATE {RewriteSqlEntityTableName(EntityTableName)} SET {RewriteName(columnName, true)} = {RewriteValue(value, type)} "; + var updateSql = $"UPDATE {AddSchema(DelimitName(RewriteName(EntityTableName)))} SET {DelimitName(RewriteName(columnName))} = {RewriteValue(value, type)} "; if (!string.IsNullOrEmpty(condition)) { + // note that condition values must be created using RewriteName(), DelimitName(), RewriteValue() if targeting multiple database platforms updateSql += $"WHERE {condition}"; } _migrationBuilder.Sql(updateSql); diff --git a/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs b/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs index 87fb34cc..07f8043b 100644 --- a/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs +++ b/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs @@ -12,14 +12,19 @@ namespace Oqtane.Migrations protected IDatabase ActiveDatabase { get; } - protected string RewriteName(string name) + protected string DelimitName(string name) { - return ActiveDatabase.RewriteName(name, false); + return ActiveDatabase.DelimitName(name); } - protected string RewriteName(string name, bool isQuery) + protected string RewriteName(string name) { - return ActiveDatabase.RewriteName(name, isQuery); + return ActiveDatabase.RewriteName(name); + } + + protected string RewriteValue(string value, string type) + { + return ActiveDatabase.RewriteValue(value, type); } } } diff --git a/Oqtane.Server/Migrations/Tenant/01000101_AddAdditionColumnToNotifications.cs b/Oqtane.Server/Migrations/Tenant/01000101_AddAdditionColumnToNotifications.cs index 96c56e67..3dc8e016 100644 --- a/Oqtane.Server/Migrations/Tenant/01000101_AddAdditionColumnToNotifications.cs +++ b/Oqtane.Server/Migrations/Tenant/01000101_AddAdditionColumnToNotifications.cs @@ -21,7 +21,7 @@ namespace Oqtane.Migrations.Tenant notificationEntityBuilder.AddDateTimeColumn("SendOn", true); //Update new Column - notificationEntityBuilder.UpdateColumn("SendOn", $"{ActiveDatabase.RewriteName("CreatedOn")}", $"{ActiveDatabase.RewriteName("SendOn")} IS NULL"); + notificationEntityBuilder.UpdateColumn("SendOn", $"{RewriteName("CreatedOn")}", $"{DelimitName(RewriteName("SendOn"))} IS NULL"); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Oqtane.Server/Migrations/Tenant/02000101_UpdateIconColumnInPage.cs b/Oqtane.Server/Migrations/Tenant/02000101_UpdateIconColumnInPage.cs index 1edbadcc..d9dcf06f 100644 --- a/Oqtane.Server/Migrations/Tenant/02000101_UpdateIconColumnInPage.cs +++ b/Oqtane.Server/Migrations/Tenant/02000101_UpdateIconColumnInPage.cs @@ -18,8 +18,8 @@ namespace Oqtane.Migrations.Tenant { ///Update Icon Field in Page var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase); - var updateSql = ActiveDatabase.ConcatenateSql("'oi oi-'", $"{ActiveDatabase.RewriteName("Icon")}"); - pageEntityBuilder.UpdateColumn("Icon", updateSql, $"{ActiveDatabase.RewriteName("Icon")} <> ''" ); + var updateSql = ActiveDatabase.ConcatenateSql("'oi oi-'", $"{DelimitName(RewriteName("Icon"))}"); + pageEntityBuilder.UpdateColumn("Icon", updateSql, $"{DelimitName(RewriteName("Icon"))} <> ''" ); } } } diff --git a/Oqtane.Server/Migrations/Tenant/02000202_UpdateDefaultContainerTypeInSitePage.cs b/Oqtane.Server/Migrations/Tenant/02000202_UpdateDefaultContainerTypeInSitePage.cs index ddf30943..1c0cb5b1 100644 --- a/Oqtane.Server/Migrations/Tenant/02000202_UpdateDefaultContainerTypeInSitePage.cs +++ b/Oqtane.Server/Migrations/Tenant/02000202_UpdateDefaultContainerTypeInSitePage.cs @@ -20,18 +20,18 @@ namespace Oqtane.Migrations.Tenant { //Update DefaultContainerType In Site var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); - siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); - siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); + siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); + siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); //Update DefaultContainerType in Page var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase); - pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); - pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); + pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); + pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); //Update ContainerType in PageModule var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase); - pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); - pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); + pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'"); + pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'"); } } diff --git a/Oqtane.Server/Migrations/Tenant/02000203_DropDefaultLayoutInSite.cs b/Oqtane.Server/Migrations/Tenant/02000203_DropDefaultLayoutInSite.cs index 068c938c..4e174feb 100644 --- a/Oqtane.Server/Migrations/Tenant/02000203_DropDefaultLayoutInSite.cs +++ b/Oqtane.Server/Migrations/Tenant/02000203_DropDefaultLayoutInSite.cs @@ -29,22 +29,22 @@ namespace Oqtane.Migrations.Tenant siteEntityBuilder.DropColumn("DefaultLayoutType"); //Update DefaultContainerType In Site - siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); - siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); + siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); + siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); //Drop Column from Page Table var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase); pageEntityBuilder.DropColumn("LayoutType"); //Update DefaultContainerType in Page - pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); - pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); + pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); + pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); //Update ContainerType in PageModule var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase); - pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); - pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); + pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'"); + pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'"); } } diff --git a/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs b/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs index 3658598c..a5ed9237 100644 --- a/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs +++ b/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs @@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); folderEntityBuilder.AddIntegerColumn("Capacity", true); folderEntityBuilder.UpdateColumn("Capacity", "0"); - folderEntityBuilder.UpdateColumn("Capacity", Constants.UserFolderCapacity.ToString(), $"{ActiveDatabase.RewriteName("Name")} = 'My Folder'"); + folderEntityBuilder.UpdateColumn("Capacity", Constants.UserFolderCapacity.ToString(), $"{DelimitName(RewriteName("Name"))} = 'My Folder'"); folderEntityBuilder.AddStringColumn("ImageSizes", 512, true, true); var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); diff --git a/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs b/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs index 2f99e341..46b52150 100644 --- a/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs +++ b/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs @@ -18,13 +18,13 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); - settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{DelimitName(RewriteName("SettingName"))} NOT LIKE 'SMTP%'"); } protected override void Down(MigrationBuilder migrationBuilder) { var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); - settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{DelimitName(RewriteName("SettingName"))} NOT LIKE 'SMTP%'"); } } } diff --git a/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs b/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs index f7e73854..dae85bb4 100644 --- a/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs +++ b/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs @@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); settingEntityBuilder.AddBooleanColumn("IsPrivate", true); settingEntityBuilder.UpdateColumn("IsPrivate", "0", "bool", ""); - settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{RewriteName("EntityName")} = 'Site' AND { RewriteName("SettingName")} LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{DelimitName(RewriteName("EntityName"))} = 'Site' AND { DelimitName(RewriteName("SettingName"))} LIKE 'SMTP%'"); settingEntityBuilder.DropColumn("IsPublic"); } diff --git a/Oqtane.Server/Migrations/Tenant/06010402_ResetTimeZone.cs b/Oqtane.Server/Migrations/Tenant/06010402_ResetTimeZone.cs new file mode 100644 index 00000000..6e614cd0 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/06010402_ResetTimeZone.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.06.01.04.02")] + public class ResetTimeZone : MultiDatabaseMigration + { + public ResetTimeZone(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + // resetting value as framework now uses IANA ID consistently for time zones + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.UpdateColumn("TimeZoneId", "''"); + + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.UpdateColumn("TimeZoneId", "''"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} From cceda1db1eae40bcc08a8a96f436c9d61a521a06 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 09:06:36 -0400 Subject: [PATCH 40/56] add OAuth support to Notification Job (#5372) --- .../Infrastructure/Jobs/NotificationJob.cs | 260 +++++++++++------- 1 file changed, 154 insertions(+), 106 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index e1d3e17a..6b755e89 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; - +using System.Threading.Tasks; using MailKit.Net.Smtp; - using Microsoft.Extensions.DependencyInjection; - +using Microsoft.Identity.Client; using MimeKit; - using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; +using MailKit.Security; namespace Oqtane.Infrastructure { @@ -27,7 +26,7 @@ namespace Oqtane.Infrastructure } // job is executed for each tenant in installation - public override string ExecuteJob(IServiceProvider provider) + public async override Task ExecuteJobAsync(IServiceProvider provider) { string log = ""; @@ -48,126 +47,175 @@ namespace Oqtane.Infrastructure if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") { - if (settingRepository.GetSettingValue(settings, "SMTPHost", "") != "" && - settingRepository.GetSettingValue(settings, "SMTPPort", "") != "" && - settingRepository.GetSettingValue(settings, "SMTPSender", "") != "") + bool valid = true; + if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") + { + // basic + if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPSender", "") == "") + { + log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "
"; + valid = false; + } + } + else + { + // oauth + if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPAuthority", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPClientId", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPClientSecret", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPScopes", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPSender", "") == "") + { + log += "SMTP Not Configured Properly In Site Settings - Host, Port, Authority, Client ID, Client Secret, Scopes, And Sender Are All Required" + "
"; + valid = false; + } + } + + + if (valid) { // construct SMTP Client using var client = new SmtpClient(); - client.Connect(host: settingRepository.GetSettingValue(settings, "SMTPHost", ""), - port: int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), - options: bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? MailKit.Security.SecureSocketOptions.StartTls : MailKit.Security.SecureSocketOptions.None); + await client.ConnectAsync(settingRepository.GetSettingValue(settings, "SMTPHost", ""), + int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), + bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? SecureSocketOptions.StartTls : SecureSocketOptions.None); - if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") + if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") { - client.Authenticate(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), - settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + // it is possible to use basic without any authentication (not recommended) + if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") + { + await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), + settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + } + } + else + { + // oauth authentication + var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settingRepository.GetSettingValue(settings, "SMTPClientId", "")) + .WithAuthority(settingRepository.GetSettingValue(settings, "SMTPAuthority", "")) + .WithClientSecret(settingRepository.GetSettingValue(settings, "SMTPClientSecret", "")) + .Build(); + try + { + var result = await confidentialClientApplication.AcquireTokenForClient(settingRepository.GetSettingValue(settings, "SMTPScopes", "").Split(',')).ExecuteAsync(); + var oauth2 = new SaslMechanismOAuth2(settingRepository.GetSettingValue(settings, "SMTPSender", ""), result.AccessToken); + await client.AuthenticateAsync(oauth2); + } + catch (Exception ex) + { + log += "SMTP Not Configured Properly In Site Settings - OAuth Token Could Not Be Retrieved From Authority - " + ex.Message + "
"; + valid = false; + } } - // iterate through undelivered notifications - int sent = 0; - List notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList(); - foreach (Notification notification in notifications) + if (valid) { - // get sender and receiver information from user object if not provided - if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null) + // iterate through undelivered notifications + int sent = 0; + List notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList(); + foreach (Notification notification in notifications) { - var user = userRepository.GetUser(notification.FromUserId.Value); - if (user != null) + // get sender and receiver information from user object if not provided + if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null) { - notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail; - notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName; - } - } - if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null) - { - var user = userRepository.GetUser(notification.ToUserId.Value); - if (user != null) - { - notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail; - notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName; - } - } - - // validate recipient - if (string.IsNullOrEmpty(notification.ToEmail) || !MailboxAddress.TryParse(notification.ToEmail, out _)) - { - log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}
"; - notification.IsDeleted = true; - notificationRepository.UpdateNotification(notification); - } - else - { - MimeMessage mailMessage = new MimeMessage(); - - // sender - if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail)) - { - if (!string.IsNullOrEmpty(notification.FromDisplayName)) + var user = userRepository.GetUser(notification.FromUserId.Value); + if (user != null) { - mailMessage.From.Add(new MailboxAddress(notification.FromDisplayName, notification.FromEmail)); + notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail; + notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName; + } + } + if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null) + { + var user = userRepository.GetUser(notification.ToUserId.Value); + if (user != null) + { + notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail; + notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName; + } + } + + // validate recipient + if (string.IsNullOrEmpty(notification.ToEmail) || !MailboxAddress.TryParse(notification.ToEmail, out _)) + { + log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}
"; + notification.IsDeleted = true; + notificationRepository.UpdateNotification(notification); + } + else + { + MimeMessage mailMessage = new MimeMessage(); + + // sender + if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail)) + { + if (!string.IsNullOrEmpty(notification.FromDisplayName)) + { + mailMessage.From.Add(new MailboxAddress(notification.FromDisplayName, notification.FromEmail)); + } + else + { + mailMessage.From.Add(new MailboxAddress("", notification.FromEmail)); + } } else { - mailMessage.From.Add(new MailboxAddress("", notification.FromEmail)); + mailMessage.From.Add(new MailboxAddress((!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name, + settingRepository.GetSettingValue(settings, "SMTPSender", ""))); + } + + // recipient + if (!string.IsNullOrEmpty(notification.ToDisplayName)) + { + mailMessage.To.Add(new MailboxAddress(notification.ToDisplayName, notification.ToEmail)); + } + else + { + mailMessage.To.Add(new MailboxAddress("", notification.ToEmail)); + } + + // subject + mailMessage.Subject = notification.Subject; + + //body + var bodyText = notification.Body; + + if (!bodyText.Contains('<') || !bodyText.Contains('>')) + { + // plain text messages should convert line breaks to HTML tags to preserve formatting + bodyText = bodyText.Replace("\n", "
"); + } + + mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8) + { + Text = bodyText + }; + + // send mail + try + { + await client.SendAsync(mailMessage); + sent++; + notification.IsDelivered = true; + notification.DeliveredOn = DateTime.UtcNow; + notificationRepository.UpdateNotification(notification); + } + catch (Exception ex) + { + // error + log += $"NotificationId: {notification.NotificationId} - {ex.Message}
"; } } - else - { - mailMessage.From.Add(new MailboxAddress((!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name, - settingRepository.GetSettingValue(settings, "SMTPSender", ""))); - } - - // recipient - if (!string.IsNullOrEmpty(notification.ToDisplayName)) - { - mailMessage.To.Add(new MailboxAddress(notification.ToDisplayName, notification.ToEmail)); - } - else - { - mailMessage.To.Add(new MailboxAddress("", notification.ToEmail)); - } - - // subject - mailMessage.Subject = notification.Subject; - - //body - var bodyText = notification.Body; - - if (!bodyText.Contains('<') || !bodyText.Contains('>')) - { - // plain text messages should convert line breaks to HTML tags to preserve formatting - bodyText = bodyText.Replace("\n", "
"); - } - - mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8) - { - Text = bodyText - }; - - // send mail - try - { - client.Send(mailMessage); - sent++; - notification.IsDelivered = true; - notification.DeliveredOn = DateTime.UtcNow; - notificationRepository.UpdateNotification(notification); - } - catch (Exception ex) - { - // error - log += $"NotificationId: {notification.NotificationId} - {ex.Message}
"; - } } + await client.DisconnectAsync(true); + log += "Notifications Delivered: " + sent + "
"; } - client.Disconnect(true); - log += "Notifications Delivered: " + sent + "
"; - } - else - { - log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "
"; } } else From 91c53098552c93ab547678056355e4235c17db36 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 10:26:18 -0400 Subject: [PATCH 41/56] fix #5372 - add support for sending SMTP emails using OAuth --- Oqtane.Client/Modules/Admin/Site/Index.razor | 252 ++++++++++++------ .../Resources/Modules/Admin/Site/Index.resx | 48 +++- 2 files changed, 215 insertions(+), 85 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index fcc9557f..8b7c9ad2 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -193,80 +193,125 @@
-
-
+
- @Localizer["Smtp.Required.EnableNotificationJob"]
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
- -
- -
-
-
- -
-
- - + @if (_smtpenabled == "True") + { +
+
+
+
+ @Localizer["Smtp.Required.EnableNotificationJob"]
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
- -

+ @if (_smtpauthentication == "Basic") + { +
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ } + else + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ } +
+ +
+ +
+
+
+ +
+ +
+
+ +

+ }
@@ -454,16 +499,23 @@ private string _headcontent = string.Empty; private string _bodycontent = string.Empty; + private string _smtpenabled = "False"; + private string _smtpauthentication = "Basic"; private string _smtphost = string.Empty; private string _smtpport = string.Empty; - private string _smtpssl = "False"; + private string _smtpssl = "True"; private string _smtpusername = string.Empty; private string _smtppassword = string.Empty; private string _smtppasswordtype = "password"; private string _togglesmtppassword = string.Empty; + private string _smtpauthority = string.Empty; + private string _smtpclientid = string.Empty; + private string _smtpclientsecret = string.Empty; + private string _smtpclientsecrettype = "password"; + private string _togglesmtpclientsecret = string.Empty; + private string _smtpscopes = string.Empty; private string _smtpsender = string.Empty; private string _smtprelay = "False"; - private string _smtpenabled = "True"; private int _retention = 30; private string _pwaisenabled; @@ -555,15 +607,21 @@ _bodycontent = site.BodyContent; // SMTP + _smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False"); _smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty); _smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); + _smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic"); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _togglesmtppassword = SharedLocalizer["ShowPassword"]; + _smtpauthority = SettingService.GetSetting(settings, "SMTPAuthority", string.Empty); + _smtpclientid = SettingService.GetSetting(settings, "SMTPClientId", string.Empty); + _smtpclientsecret = SettingService.GetSetting(settings, "SMTPClientSecret", string.Empty); + _togglesmtpclientsecret = SharedLocalizer["ShowPassword"]; + _smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False"); - _smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True"); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); // PWA @@ -744,8 +802,13 @@ settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); + settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); + settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true); + settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true); + settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true); + settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true); settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true); @@ -812,6 +875,46 @@ } } + private void SMTPAuthenticationChanged(ChangeEventArgs e) + { + _smtpauthentication = (string)e.Value; + StateHasChanged(); + } + + private void SMTPEnabledChanged(ChangeEventArgs e) + { + _smtpenabled = (string)e.Value; + StateHasChanged(); + } + + private void ToggleSMTPPassword() + { + if (_smtppasswordtype == "password") + { + _smtppasswordtype = "text"; + _togglesmtppassword = SharedLocalizer["HidePassword"]; + } + else + { + _smtppasswordtype = "password"; + _togglesmtppassword = SharedLocalizer["ShowPassword"]; + } + } + + private void ToggleSmtpClientSecret() + { + if (_smtpclientsecrettype == "password") + { + _smtpclientsecrettype = "text"; + _togglesmtpclientsecret = SharedLocalizer["HidePassword"]; + } + else + { + _smtpclientsecrettype = "password"; + _togglesmtpclientsecret = SharedLocalizer["ShowPassword"]; + } + } + private async Task SendEmail() { if (_smtphost != "" && _smtpport != "" && _smtpsender != "") @@ -822,8 +925,13 @@ settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); + settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); + settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true); + settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true); + settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true); + settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await logger.LogInformation("Site SMTP Settings Saved"); @@ -844,20 +952,6 @@ } } - private void ToggleSMTPPassword() - { - if (_smtppasswordtype == "password") - { - _smtppasswordtype = "text"; - _togglesmtppassword = SharedLocalizer["HidePassword"]; - } - else - { - _smtppasswordtype = "password"; - _togglesmtppassword = SharedLocalizer["ShowPassword"]; - } - } - private async Task GetAliases() { if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index acfd022e..7e2fae82 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -192,7 +192,7 @@ Enter the port number for the SMTP server. Please note this field is required if you provide a host name. - + Specify if SSL is required for your SMTP server @@ -202,7 +202,7 @@ Enter the password for your SMTP account - Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server. + Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP server. Select whether you would like this site to be available as a Progressive Web Application (PWA) @@ -240,8 +240,8 @@ Port: - - SSL Enabled: + + SSL Required: Username: @@ -372,10 +372,10 @@ Page Content - + Specify if SMTP is enabled for this site - + Enabled? @@ -453,4 +453,40 @@ The default time zone for the site + + Basic + + + OAuth + + + Authentication: + + + Specify the SMTP authentication type + + + Client ID: + + + The Client ID for the SMTP provider + + + Client Secret: + + + The Client Secret for the SMTP provider + + + Scopes: + + + A list of Scopes for the SMTP provider (separated by commas) + + + Authority Url: + + + The Authority Url for the SMTP provider + \ No newline at end of file From e179976fe8478bb7c5d95d1a8d3a1840aca1d90d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 17:00:27 -0400 Subject: [PATCH 42/56] improve TimeZoneService --- .../Resources/TimeZoneResources.resx | 9 ----- Oqtane.Client/Services/TimeZoneService.cs | 38 ++++++++++++++----- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Oqtane.Client/Resources/TimeZoneResources.resx b/Oqtane.Client/Resources/TimeZoneResources.resx index d736dcea..1af7de15 100644 --- a/Oqtane.Client/Resources/TimeZoneResources.resx +++ b/Oqtane.Client/Resources/TimeZoneResources.resx @@ -117,13 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - (UTC) Coordinated Universal Time - - - (UTC-05:00) Eastern Time (US & Canada) - - - (UTC-08:00) Pacific Time (US & Canada) - \ No newline at end of file diff --git a/Oqtane.Client/Services/TimeZoneService.cs b/Oqtane.Client/Services/TimeZoneService.cs index f6cab35b..753e455b 100644 --- a/Oqtane.Client/Services/TimeZoneService.cs +++ b/Oqtane.Client/Services/TimeZoneService.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Localization; +using NodaTime.TimeZones; +using NodaTime; using Oqtane.Documentation; -using Oqtane.Models; -using Oqtane.Shared; +using NodaTime.Extensions; namespace Oqtane.Services { @@ -17,18 +19,36 @@ namespace Oqtane.Services _TimeZoneLocalizer = TimeZoneLocalizer; } - public List GetTimeZones() + public List GetTimeZones() { - var _timezones = new List(); - foreach (var timezone in Utilities.GetTimeZones()) + var timezones = new List(); + + foreach (var tz in DateTimeZoneProviders.Tzdb.GetAllZones() + // only include timezones which have a country code defined or are US timezones + .Where(item => !string.IsNullOrEmpty(TzdbDateTimeZoneSource.Default.ZoneLocations.FirstOrDefault(l => l.ZoneId == item.Id)?.CountryCode) || item.Id.ToLower().Contains("us/")) + // order by UTC offset (ie. -11:00 to +14:00) + .OrderBy(item => item.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks)) { - _timezones.Add(new TimeZone + // get localized display name + var displayname = _TimeZoneLocalizer[tz.Id].Value; + if (displayname == tz.Id) { - Id = timezone.Id, - DisplayName = _TimeZoneLocalizer[timezone.Id] + // use default "friendly" display format + displayname = displayname.Replace("_", " ").Replace("/", " / "); + } + + // include offset prefix + var offset = tz.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks; + displayname = "(UTC" + (offset >= 0 ? "+" : "-") + new DateTime(Math.Abs(offset)).ToString("HH:mm") + ") " + displayname; + + timezones.Add(new Models.TimeZone() + { + Id = tz.Id, + DisplayName = displayname }); } - return _timezones.OrderBy(item => item.DisplayName).ToList(); + + return timezones; } } } From 9f097521f6179f0370070c1e5674c09fa9238b0e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 08:11:42 -0400 Subject: [PATCH 43/56] fix #5348 - ensure time zones work consistently on all platforms --- Oqtane.Client/Modules/Admin/Register/Index.razor | 3 ++- Oqtane.Client/Modules/Admin/Site/Index.razor | 3 ++- Oqtane.Client/Modules/Admin/Users/Add.razor | 3 ++- Oqtane.Client/Modules/Admin/Users/Edit.razor | 3 ++- Oqtane.Shared/Shared/Utilities.cs | 10 ---------- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 025c86c0..2f7c47e7 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -6,6 +6,7 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @if (_initialized) { @@ -114,7 +115,7 @@ { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.Site.TimeZoneId; _initialized = true; } diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 8b7c9ad2..67316ae4 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -10,6 +10,7 @@ @inject IAliasService AliasService @inject IThemeService ThemeService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @@ -559,7 +560,7 @@ Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (site != null) { - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 2387e88e..cb5c7224 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -5,6 +5,7 @@ @inject IUserService UserService @inject IProfileService ProfileService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -132,7 +133,7 @@ { try { - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _settings = new Dictionary(); _timezoneid = PageState.Site.TimeZoneId; diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 002e64df..353a474f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject IFileService FileService +@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -203,7 +204,7 @@ _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _togglepassword = SharedLocalizer["ShowPassword"]; _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) { diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index f5faff87..44cccffc 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -692,16 +692,6 @@ namespace Oqtane.Shared return (localDateTime?.Date, localTime); } - public static List GetTimeZones() - { - return [.. DateTimeZoneProviders.Tzdb.GetAllZones() - .Select(tz => new TimeZone() - { - Id = tz.Id, - DisplayName = tz.Id - })]; - } - public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate) { DateTime currentUtcTime = DateTime.UtcNow; From b1770ebb762bc85567c001c7379875434099651e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 08:40:38 -0400 Subject: [PATCH 44/56] fix #5346 - deleting role should remove associated permissions --- Oqtane.Server/Repository/RoleRepository.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Oqtane.Server/Repository/RoleRepository.cs b/Oqtane.Server/Repository/RoleRepository.cs index 1c4aa0c4..e9d1590d 100644 --- a/Oqtane.Server/Repository/RoleRepository.cs +++ b/Oqtane.Server/Repository/RoleRepository.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Oqtane.Models; +using Oqtane.Modules.Admin.Users; +using Oqtane.Shared; namespace Oqtane.Repository { @@ -71,7 +73,19 @@ namespace Oqtane.Repository public void DeleteRole(int roleId) { using var db = _dbContextFactory.CreateDbContext(); + Role role = db.Role.Find(roleId); + + // remove permissions for this role + var permissions = db.Permission.Where(item => item.SiteId == role.SiteId).ToList(); + foreach (var permission in permissions) + { + if (permission.RoleId == roleId) + { + db.Permission.Remove(permission); + } + } + db.Role.Remove(role); db.SaveChanges(); } From 658059806bfbe8ddca058ecd2da589d0babddd61 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 09:05:37 -0400 Subject: [PATCH 45/56] fix #5346 - deleting role should remove associated useroles --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 2 +- Oqtane.Server/Repository/RoleRepository.cs | 19 ++++++++++--------- Oqtane.Server/Repository/UserRepository.cs | 8 ++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 353a474f..cc497086 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -159,7 +159,7 @@ } @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") { - + }

diff --git a/Oqtane.Server/Repository/RoleRepository.cs b/Oqtane.Server/Repository/RoleRepository.cs index e9d1590d..3d16eb35 100644 --- a/Oqtane.Server/Repository/RoleRepository.cs +++ b/Oqtane.Server/Repository/RoleRepository.cs @@ -74,18 +74,19 @@ namespace Oqtane.Repository { using var db = _dbContextFactory.CreateDbContext(); - Role role = db.Role.Find(roleId); - - // remove permissions for this role - var permissions = db.Permission.Where(item => item.SiteId == role.SiteId).ToList(); - foreach (var permission in permissions) + // remove userroles for role + foreach (var userrole in db.UserRole.Where(item => item.RoleId == roleId)) { - if (permission.RoleId == roleId) - { - db.Permission.Remove(permission); - } + db.UserRole.Remove(userrole); } + // remove permissions for role + foreach (var permission in db.Permission.Where(item => item.RoleId == roleId)) + { + db.Permission.Remove(permission); + } + + Role role = db.Role.Find(roleId); db.Role.Remove(role); db.SaveChanges(); } diff --git a/Oqtane.Server/Repository/UserRepository.cs b/Oqtane.Server/Repository/UserRepository.cs index 3c0a40ad..4858e7c3 100644 --- a/Oqtane.Server/Repository/UserRepository.cs +++ b/Oqtane.Server/Repository/UserRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Oqtane.Models; +using Oqtane.Modules.Admin.Users; using Oqtane.Shared; namespace Oqtane.Repository @@ -131,6 +132,13 @@ namespace Oqtane.Repository public void DeleteUser(int userId) { using var db = _dbContextFactory.CreateDbContext(); + + // remove permissions for user + foreach (var permission in db.Permission.Where(item => item.UserId == userId)) + { + db.Permission.Remove(permission); + } + var user = db.User.Find(userId); db.User.Remove(user); db.SaveChanges(); From f4cea3fe03cd1aea9c19fbedf732eaf9b04ffb98 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 16:20:07 -0400 Subject: [PATCH 46/56] fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site --- Oqtane.Client/Modules/Admin/Login/Index.razor | 12 +--- Oqtane.Client/Modules/Admin/Users/Add.razor | 11 ++++ Oqtane.Client/Modules/Admin/Users/Edit.razor | 2 +- Oqtane.Client/Modules/Admin/Users/Index.razor | 14 ++++- .../Resources/Modules/Admin/Login/Index.resx | 2 +- .../Resources/Modules/Admin/Users/Add.resx | 6 ++ .../Resources/Modules/Admin/Users/Edit.resx | 2 +- .../Resources/Modules/Admin/Users/Index.resx | 8 ++- Oqtane.Server/Controllers/UserController.cs | 7 +-- .../OqtaneServiceCollectionExtensions.cs | 5 +- Oqtane.Server/Managers/UserManager.cs | 63 +++++++++++-------- .../Interfaces/ISettingRepository.cs | 1 + Oqtane.Server/Repository/SettingRepository.cs | 13 ++++ 13 files changed, 101 insertions(+), 45 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f0a057d0..66562e6c 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -21,9 +21,7 @@ else @if (_allowexternallogin) { -
- -
+

} @if (_allowsitelogin) { @@ -49,15 +47,11 @@ else -
- -
+

@if (PageState.Site.AllowRegistration) { -
- -
+

@Localizer["Register"] } } diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index cb5c7224..e5581222 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -28,6 +28,15 @@ +
+ +
+ +
+
@@ -120,6 +129,7 @@ private bool _initialized = false; private string _username = string.Empty; private string _email = string.Empty; + private string _confirmed = "True"; private string _displayname = string.Empty; private string _timezoneid = string.Empty; private string _notify = "True"; @@ -169,6 +179,7 @@ user.Username = _username; user.Password = ""; // will be auto generated user.Email = _email; + user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; user.PhotoFileId = null; diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index cc497086..2d75d27a 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -48,7 +48,7 @@
- +
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
- +
+ @@ -499,7 +499,7 @@ else private string _allowregistration; private string _registerurl; private string _profileurl; - private string _requirevalidemail; + private string _requireconfirmedemail; private string _twofactor; private string _cookiename; private string _cookieexpiration; @@ -570,7 +570,7 @@ else _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); - _requirevalidemail = SettingService.GetSetting(settings, "LoginOptions:RequireValidEmail", "true"); + _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true"); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { @@ -696,7 +696,7 @@ else { settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); - settings = SettingService.SetSetting(settings, "LoginOptions:RequireValidEmail", _requirevalidemail, false); + settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 2bbf1f75..79144098 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -372,11 +372,11 @@ Two Factor Authentication? - - Do you want to require registered users to validate their email address before they are allowed to log in? + + Do you want to require registered users to verify their email address before they are allowed to log in? - - Require Valid Email? + + Require Verified Email? Disabled diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 7a60e785..5f9cd8dc 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -180,7 +180,7 @@ namespace Oqtane.Managers if (User != null) { string siteName = _sites.GetSite(user.SiteId).Name; - if (!user.EmailConfirmed && bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true"))) + if (!user.EmailConfirmed && bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true"))) { string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); @@ -252,7 +252,7 @@ namespace Oqtane.Managers await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated } - if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true"))) + if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true"))) { if (user.EmailConfirmed) { @@ -379,7 +379,7 @@ namespace Oqtane.Managers } else { - if (!bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true")) || await _identityUserManager.IsEmailConfirmedAsync(identityuser)) + if (!bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")) || await _identityUserManager.IsEmailConfirmedAsync(identityuser)) { user = GetUser(identityuser.UserName, alias.SiteId); if (user != null) From 6c0e2a62e7af7285b194c96ac94cafeec2565644 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 30 Jul 2025 12:53:59 +0200 Subject: [PATCH 48/56] Discussion #5426 updated and returned to https://cdnjs.com/ Updated and styles tested - reload.js needs still testing? --- .../Themes/Templates/External/Client/ThemeInfo.cs | 2 +- Oqtane.Shared/Shared/Constants.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs index d4d395cd..5517a501 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -16,7 +16,7 @@ namespace [Owner].Theme.[Theme] ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane", Resources = new List() { - // obtained from https://www.jsdelivr.com/ + // obtained from https://cdnjs.com/libraries new Script(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 10be4133..c24d54a5 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -85,11 +85,11 @@ namespace Oqtane.Shared public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" }; public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client"; - - public const string BootstrapScriptUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"; - public const string BootstrapScriptIntegrity = "sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq"; - public const string BootstrapStylesheetUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"; - public const string BootstrapStylesheetIntegrity = "sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"; + //Obtained from https://cdnjs.com/libraries/bootstrap + public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/js/bootstrap.bundle.min.js"; + public const string BootstrapScriptIntegrity = "sha512-Tc0i+vRogmX4NN7tuLbQfBxa8JkfUSAxSFVzmU31nVdHyiHElPPy2cWfFacmCJKw0VqovrzKhdd2TSTMdAxp2g=="; + public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css"; + public const string BootstrapStylesheetIntegrity = "sha512-fw7f+TcMjTb7bpbLJZlP8g2Y4XcCyFZW8uy8HsRZsH/SwbMw0plKHFHr99DN3l04VsYNwvzicUX/6qurvIxbxw=="; public const string CookieConsentCookieName = "Oqtane.CookieConsent"; public const string CookieConsentCookieValue = "yes"; From cf9b4b869cf23e5d2e5fdffe6c47b91b5f115ac7 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 08:16:07 -0400 Subject: [PATCH 49/56] use margin rather than padding --- Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor | 4 ++-- Oqtane.Client/Modules/Admin/Themes/Index.razor | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor index fcfe6114..bbb2698e 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor @@ -17,8 +17,8 @@ else
- - + +
- +
diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 7e2fae82..97e4e8f3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -456,8 +456,8 @@ Basic - - OAuth + + OAuth 2.0 (OAuth2) Authentication: diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 79144098..1a008727 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -508,7 +508,7 @@ Info - OAuth 2.0 + OAuth 2.0 (OAuth2) OpenID Connect (OIDC) From 662a1817f23fd09a7222c8cbc9f2dd08d02284ff Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 10:43:36 -0400 Subject: [PATCH 54/56] fix #5364 - add ability to specify preferred Container per Pane --- Oqtane.Client/UI/ContainerBuilder.razor | 7 +++++++ Oqtane.Client/UI/Pane.razor | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/Oqtane.Client/UI/ContainerBuilder.razor b/Oqtane.Client/UI/ContainerBuilder.razor index 76565c6b..bc9b1198 100644 --- a/Oqtane.Client/UI/ContainerBuilder.razor +++ b/Oqtane.Client/UI/ContainerBuilder.razor @@ -31,6 +31,9 @@ [Parameter] public Module ModuleState { get; set; } + [Parameter] + public string ContainerType { get; set; } + protected override bool ShouldRender() { return PageState?.RenderId == ModuleState?.RenderId; @@ -44,6 +47,10 @@ protected override void OnParametersSet() { string container = ModuleState.ContainerType; + if (!string.IsNullOrEmpty(ContainerType)) + { + container = ContainerType; + } if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer) { container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer; diff --git a/Oqtane.Client/UI/Pane.razor b/Oqtane.Client/UI/Pane.razor index 633cd4fd..3e5ddfd3 100644 --- a/Oqtane.Client/UI/Pane.razor +++ b/Oqtane.Client/UI/Pane.razor @@ -26,6 +26,9 @@ else [Parameter] public string Name { get; set; } + [Parameter] + public string ContainerType { get; set; } + RenderFragment DynamicComponent { get; set; } protected override void OnParametersSet() @@ -119,6 +122,7 @@ else { builder.OpenComponent(0, typeof(ContainerBuilder)); builder.AddAttribute(1, "ModuleState", module); + builder.AddAttribute(2, "ContainerType", ContainerType); builder.SetKey(module.PageModuleId); builder.CloseComponent(); } From bfe57c3ac7c60313fdeaeaaa407edd564b2f83f6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 13:35:39 -0400 Subject: [PATCH 55/56] synchronize interop,js with .NET MAUI --- Oqtane.Maui/wwwroot/js/interop.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 191d9823..fecc4c99 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -311,7 +311,7 @@ Oqtane.Interop = { } return files; }, - uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { + uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) { var success = true; var fileinput = document.getElementById('FileInput_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id); @@ -344,16 +344,22 @@ Oqtane.Interop = { const totalParts = Math.ceil(file.size / chunkSize); let partCount = 0; + let filename = file.name; + if (anonymizeuploadfilenames) { + filename = crypto.randomUUID() + '.' + filename.split('.').pop(); + } + const uploadPart = () => { const start = partCount * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); return new Promise((resolve, reject) => { + let formdata = new FormData(); formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('folder', folder); - formdata.append('formfile', chunk, file.name); + formdata.append('formfile', chunk, filename); var credentials = 'same-origin'; var headers = new Headers(); From eae8b431eeb9f4b457a7a67f2e68cf19926148b4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 13:40:25 -0400 Subject: [PATCH 56/56] synchronize app.css with .NET MAUI --- Oqtane.Maui/wwwroot/css/app.css | 17 ++++++++++++++--- Oqtane.Server/wwwroot/css/app.css | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css index ab9c6adb..46e749f7 100644 --- a/Oqtane.Maui/wwwroot/css/app.css +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -239,18 +239,19 @@ app { .app-form-inline { display: inline; } -.app-search{ + +.app-search { display: inline-block; position: relative; } -.app-search input + button{ +.app-search input + button { background: none; border: none; position: absolute; right: 0; top: 0; } -.app-search input + button .oi{ +.app-search input + button .oi { top: 0; } .app-search-noinput { @@ -275,3 +276,13 @@ app { .app-logo .navbar-brand { padding: 5px 20px 5px 20px; } + +/* cookie consent */ +.gdpr-consent-bar .btn-show { + bottom: -3px; + left: 5px; +} +.gdpr-consent-bar .btn-hide { + top: 0; + right: 5px; +} diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index 00461f64..46e749f7 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -278,11 +278,11 @@ app { } /* cookie consent */ -.gdpr-consent-bar .btn-show{ +.gdpr-consent-bar .btn-show { bottom: -3px; left: 5px; } -.gdpr-consent-bar .btn-hide{ +.gdpr-consent-bar .btn-hide { top: 0; right: 5px; }