From 874d9f32a9f00edfd4168e826f3fb0be116be360 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 2 May 2025 12:18:45 +0200 Subject: [PATCH 01/48] Updated fBootstrap 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 b1cc0ffc13c2107bdd5c4d7a308df8b21baa0630 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 3 Oct 2025 18:38:07 +0200 Subject: [PATCH 02/48] Fixed the cropped glow on the Oqtane logo --- oqtane.png | Bin 63337 -> 63750 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/oqtane.png b/oqtane.png index d62a37dc66a67c9ff3cd0605a39dd3bb0e967ebf..19e9e4d6d9a314615d8d79316ca00923fc22b9d7 100644 GIT binary patch literal 63750 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 literal 63337 zcmXV1Wk6J2*F``|8kGi-mJaDsx25)zKQoRkI<5;6esJrM&9@g5u#oQ?Q+;iMt^8L0wH zv4{A9YW7L_6B1H&%&P~(mx$k(_Hx=zNJxYZÃi=NR)NY_vDQlGxM>mM$mr_g_^ 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 From e58ee4e5b1f7ddd3cc5e3f704b12d7f491487da1 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 7 Nov 2025 14:40:14 +0800 Subject: [PATCH 03/48] Display error message when missing injected services. --- .../Resources/UI/ModuleInstance.resx | 5 +- Oqtane.Client/UI/RenderModeBoundary.razor | 89 +++++++++++++------ Oqtane.Shared/Shared/Utilities.cs | 71 +++++++++++++++ 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/Oqtane.Client/Resources/UI/ModuleInstance.resx b/Oqtane.Client/Resources/UI/ModuleInstance.resx index fc0994f2..4c4a4c63 100644 --- a/Oqtane.Client/Resources/UI/ModuleInstance.resx +++ b/Oqtane.Client/Resources/UI/ModuleInstance.resx @@ -124,6 +124,9 @@ Module Type Is Invalid For {0} - An Unexpected Error Has Occurred + An Unexpected Error Has Occurred + + + Missing service(s): {0}. Please make sure they have been registered correctly. \ No newline at end of file diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor index abf96087..b6b5e0af 100644 --- a/Oqtane.Client/UI/RenderModeBoundary.razor +++ b/Oqtane.Client/UI/RenderModeBoundary.razor @@ -1,7 +1,11 @@ @namespace Oqtane.UI +@using System.Reflection +@using Module = Oqtane.Models.Module +@inject IServiceProvider ServiceProvider @inject SiteState ComponentSiteState @inject IStringLocalizer Localizer @inject ILogService LoggingService +@inject NavigationManager NavigationManager @inherits ErrorBoundary @@ -67,37 +71,50 @@ { if (ShouldRender()) { - if (!string.IsNullOrEmpty(ModuleState.ModuleType)) - { - ModuleType = Type.GetType(ModuleState.ModuleType); - if (ModuleType != null) - { - // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary) - ComponentSiteState.Hydrate(SiteState); - - DynamicComponent = builder => - { - builder.OpenComponent(0, ModuleType); - builder.AddAttribute(1, "RenderModeBoundary", this); - builder.CloseComponent(); - }; - } - else - { - // module does not exist with typename specified - _messageContent = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0)); - _messageType = MessageType.Error; - _messagePosition = "top"; - _messageStyle = MessageStyle.Alert; - } - } - else + if (string.IsNullOrEmpty(ModuleState.ModuleType)) { _messageContent = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName); _messageType = MessageType.Error; _messagePosition = "top"; _messageStyle = MessageStyle.Alert; + + return; } + + ModuleType = Type.GetType(ModuleState.ModuleType); + var moduleName = Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0); + if (ModuleType == null) + { + // module does not exist with typename specified + _messageContent = string.Format(Localizer["Error.Module.InvalidName"], moduleName); + _messageType = MessageType.Error; + _messagePosition = "top"; + _messageStyle = MessageStyle.Alert; + + return; + } + + //only validate the services injection in development environment + if (NavigationManager.BaseUri.Contains("localhost:") && !ValidateModuleTypeInjectedServices(ModuleType, out IList missingServices)) + { + // module type is not valid for instantiation + _messageContent = string.Format(Localizer["Error.Module.InvalidInjectedServices"], string.Join(",", missingServices)); + _messageType = MessageType.Error; + _messagePosition = "top"; + _messageStyle = MessageStyle.Alert; + + return; + } + + // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary) + ComponentSiteState.Hydrate(SiteState); + + DynamicComponent = builder => + { + builder.OpenComponent(0, ModuleType); + builder.AddAttribute(1, "RenderModeBoundary", this); + builder.CloseComponent(); + }; } } @@ -165,4 +182,26 @@ _error = ""; base.Recover(); } + + private bool ValidateModuleTypeInjectedServices(Type moduleType, out IList missingServices) + { + missingServices = new List(); + + var properties = Utilities.GetPropertiesIncludingInherited(moduleType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach(var property in properties) + { + var injectAttribute = property.GetCustomAttribute(typeof(InjectAttribute)); + if (injectAttribute != null) + { + var serviceType = property.PropertyType; + var service = ServiceProvider.GetService(serviceType); + if (serviceType != null && service == null) + { + missingServices.Add(Utilities.GetTypeNameLastSegment(serviceType.FullName, 0)); + } + } + } + + return !missingServices.Any(); + } } \ No newline at end of file diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 44cccffc..45888f0c 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; @@ -750,6 +752,75 @@ namespace Oqtane.Shared } } + public static IEnumerable GetPropertiesIncludingInherited(Type type, BindingFlags bindingFlags) + { + var dictionary = new Dictionary(StringComparer.Ordinal); + + var currentType = type; + while (currentType != null) + { + var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly); + foreach (var property in properties) + { + if (!dictionary.TryGetValue(property.Name, out var others)) + { + dictionary.Add(property.Name, property); + } + else if (!IsInheritedProperty(property, others)) + { + List many; + if (others is PropertyInfo single) + { + many = new List { single }; + dictionary[property.Name] = many; + } + else + { + many = (List)others; + } + many.Add(property); + } + } + + currentType = currentType.BaseType; + } + + foreach (var item in dictionary) + { + if (item.Value is PropertyInfo property) + { + yield return property; + continue; + } + + var list = (List)item.Value; + var count = list.Count; + for (var i = 0; i < count; i++) + { + yield return list[i]; + } + } + } + + private static bool IsInheritedProperty(PropertyInfo property, object others) + { + if (others is PropertyInfo single) + { + return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition(); + } + + var many = (List)others; + foreach (var other in CollectionsMarshal.AsSpan(many)) + { + if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition()) + { + return true; + } + } + + return false; + } + [Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)] public static string ContentUrl(Alias alias, int fileId) { From 583bd3b5117fe2041ef1cbb1b71f80effb8737f3 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 09:29:06 -0500 Subject: [PATCH 04/48] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d7709a3..95336847 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,11 @@ A free ASP.NET hosting account. No hidden fees. No credit card required. **Installing using the Oqtane Application Template:** -(Note that "MyCompany.MyProject" can be replaced with your own unique company and project name) +If you have an older version of the Oqtane Application Template installed and want to use the latest, use the following .NERT CLI command to uninstall the old version: +``` +dotnet new uninstall Oqtane.Application.Template +``` +To install the Oqtane Application Template and create a new project, use the following .NET CLI commands (note that "MyCompany.MyProject" can be replaced with your own unique company and project name): ``` dotnet new install Oqtane.Application.Template From 708d79ffaf4e825985ea6a7d1d796b6e40165e66 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 09:29:45 -0500 Subject: [PATCH 05/48] Fix filename extension in README for solution file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95336847..f153d12e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ dotnet run ``` - Browse to the Url specified to run the application (an Installation Wizard screen will be displayed the first time you run the application) -- To develop/debug the application in an IDE, open the *.sln file in the root folder and hit F5 +- To develop/debug the application in an IDE, open the *.slnx file in the root folder and hit F5 **Installing using source code from the Dev/Master branch:** From f0d4a416bed8d07575b9d0730bd37a95ca646c5e Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 09:33:54 -0500 Subject: [PATCH 06/48] Clarify cloning instructions for Oqtane repository Updated instructions for cloning Oqtane source code. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f153d12e..015737c1 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,10 @@ dotnet run - Install the latest edition of [Visual Studio 2026](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**. -- Clone (or download) the Oqtane Master or Dev branch source code to your local system. +- Clone (or download) the Oqtane source code to your local system: + + - Dev Branch: git clone https://github.com/oqtane/oqtane.framework + - Master Branch: git clone --single-branch --branch master https://github.com/oqtane/oqtane.framework - Open the **Oqtane.slnx** solution file (make sure you specify Oqtane.Server as the Startup Project) From 950a9bf2fa5f5c011e0531dcadbe50642a4981bd Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 11:20:08 -0500 Subject: [PATCH 07/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 015737c1..892b6717 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 -[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) was released on November 14, 2025 and is a maintenance release including 77 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) was released on November 14, 2025 and is a major release including 77 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! From 7f7c53dabe4ec08d4f95800e763f4f95899bdab6 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 11:20:47 -0500 Subject: [PATCH 08/48] Fix command reference in README for uninstalling template Corrected the command reference from .NERT to .NET CLI. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 892b6717..55054924 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A free ASP.NET hosting account. No hidden fees. No credit card required. **Installing using the Oqtane Application Template:** -If you have an older version of the Oqtane Application Template installed and want to use the latest, use the following .NERT CLI command to uninstall the old version: +If you have an older version of the Oqtane Application Template installed and want to use the latest, use the following .NET CLI command to uninstall the old version: ``` dotnet new uninstall Oqtane.Application.Template ``` From 28c7617227e04244511e054cedecee11c72f8a41 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 11:22:40 -0500 Subject: [PATCH 09/48] Refine instructions for submitting pull requests --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55054924..a07cf1e1 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ dotnet run - If you have already installed a previous version of Oqtane and you wish to do a clean database install, simply reset the DefaultConnection value in the Oqtane.Server\appsettings.json file to "". This will trigger a re-install when you run the application which will execute the database installation. -- If you want to submit pull requests make sure you install the [Github Extension For Visual Studio](https://visualstudio.github.com/). It is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json" +- If you want to submit pull requests it is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json" **Video Series** From 31b8080a3d743ea65c6260bc448f3e4ec5403c33 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 17 Nov 2025 11:24:49 -0500 Subject: [PATCH 10/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a07cf1e1..f15b4995 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ This project is open source, and therefore is a work in progress... ➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html) # Background -Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules. +Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and was inspired by his earlier efforts creating the DotNetNuke web application framework for the .NET Framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules. # Reference Implementations From dafbae72379be2a11c8dd342aade5e751d8839b2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 19 Nov 2025 10:47:38 -0500 Subject: [PATCH 11/48] initialize the Owner name when using an Oqtane Application and creating new modules or themes --- .../Modules/Admin/ModuleDefinitions/Create.razor | 11 +++++++++++ Oqtane.Client/Modules/Admin/Themes/Create.razor | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor index 4decf197..4eebbbcb 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor @@ -1,6 +1,7 @@ @namespace Oqtane.Modules.Admin.ModuleDefinitions @inherits ModuleBase @using System.Text.RegularExpressions +@using System.Reflection @inject NavigationManager NavigationManager @inject IModuleDefinitionService ModuleDefinitionService @inject IModuleService ModuleService @@ -97,6 +98,16 @@ { AddModuleMessage(Localizer["Info.Module.Development"], MessageType.Info); } + else + { + var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name; + if (entryAssemblyName.EndsWith(".Oqtane")) + { + // Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane + string[] segments = entryAssemblyName.Split('.'); + _owner = string.Join(".", segments, 0, segments.Length - 2); + } + } } protected override async Task OnParametersSetAsync() diff --git a/Oqtane.Client/Modules/Admin/Themes/Create.razor b/Oqtane.Client/Modules/Admin/Themes/Create.razor index f10ff2bf..9006dfd0 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Create.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Create.razor @@ -1,6 +1,7 @@ @namespace Oqtane.Modules.Admin.Themes @inherits ModuleBase @using System.Text.RegularExpressions +@using System.Reflection @inject NavigationManager NavigationManager @inject IThemeService ThemeService @inject IModuleService ModuleService @@ -88,6 +89,16 @@ { AddModuleMessage(Localizer["Info.Theme.CreatorIntent"], MessageType.Info); } + else + { + var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name; + if (entryAssemblyName.EndsWith(".Oqtane")) + { + // Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane + string[] segments = entryAssemblyName.Split('.'); + _owner = string.Join(".", segments, 0, segments.Length - 2); + } + } } protected override async Task OnParametersSetAsync() From 012b7ba6ed6df6184103c970a861fd2743800504 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 20 Nov 2025 19:03:26 +0800 Subject: [PATCH 12/48] Fix #5649: handle not found request. --- Oqtane.Client/UI/PageState.cs | 5 +- Oqtane.Client/UI/SiteRouter.razor | 5 +- Oqtane.Server/Components/App.razor | 14 +++- .../ApplicationBuilderExtensions.cs | 75 +++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs index 91cf158c..8aac9627 100644 --- a/Oqtane.Client/UI/PageState.cs +++ b/Oqtane.Client/UI/PageState.cs @@ -29,6 +29,8 @@ namespace Oqtane.UI public bool Refresh { get; set; } public bool AllowCookies { get; set; } + public int? StatusCode { get; set; } + public List Pages { get { return Site?.Pages; } @@ -63,7 +65,8 @@ namespace Oqtane.UI IsInternalNavigation = IsInternalNavigation, RenderId = RenderId, Refresh = Refresh, - AllowCookies = AllowCookies + AllowCookies = AllowCookies, + StatusCode = StatusCode }; } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index a345a7f0..e7b11256 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -158,7 +158,9 @@ // verify user is authenticated for current site var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey)) + if (authState.User.Identity.IsAuthenticated + && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey) + && PageState.StatusCode != (int)HttpStatusCode.NotFound) { // get user var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value); @@ -337,6 +339,7 @@ IsInternalNavigation = _isInternalNavigation, RenderId = renderid, Refresh = false, + StatusCode = PageState?.StatusCode, AllowCookies = _allowCookies.GetValueOrDefault(true) }; OnStateChange?.Invoke(_pagestate); diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 7382976b..333ed42f 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -170,6 +170,7 @@ if (page == null || page.IsDeleted) { HandlePageNotFound(site, page, route); + return; } else { @@ -248,6 +249,7 @@ IsInternalNavigation = false, RenderId = Guid.NewGuid(), Refresh = true, + StatusCode = Context.Response.StatusCode, AllowCookies = _allowCookies }; } @@ -300,8 +302,16 @@ { if (route.PagePath != "404") { - // redirect to 404 page - NavigationManager.NavigateTo(route.SiteUrl + "/404", true); + // handle not found request in static mode + if(_renderMode == RenderModes.Static) + { + NavigationManager.NotFound(); + } + else + { + // redirect to 404 page + NavigationManager.NavigateTo(route.SiteUrl + "/404", true); + } } } } diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index fa3835ea..f3d11dc1 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -1,8 +1,11 @@ using System; +using System.IO; using System.Linq; +using System.Net; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -65,6 +68,7 @@ namespace Oqtane.Extensions app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); + app.UseNotFoundResponse(); // execute any IServerStartup logic app.ConfigureOqtaneAssemblies(environment); @@ -146,5 +150,76 @@ namespace Oqtane.Extensions public static IApplicationBuilder UseExceptionMiddleWare(this IApplicationBuilder builder) => builder.UseMiddleware(); + + public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app) + { + const string notFoundRoute = "/404"; + app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true); + + app.Use(async (context, next) => + { + var path = context.Request.Path.Value ?? string.Empty; + if (ShouldSkipStatusCodeReExecution(path)) + { + var feature = context.Features.Get(); + feature?.Enabled = false; + } + + await next(); + }); + + app.Use(async (context, next) => + { + var feature = context.Features.Get(); + var handled = false; + if (feature != null + && context.Response.StatusCode == (int)HttpStatusCode.NotFound + && notFoundRoute.Equals(context.Request.Path.Value, StringComparison.OrdinalIgnoreCase)) + { + var alias = context.GetAlias(); + if (!string.IsNullOrEmpty(alias?.Path)) + { + var originalPath = context.Request.Path; + context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}"); + try + { + handled = true; + await next(); + } + finally + { + context.Request.Path = originalPath; + } + } + } + + if (!handled) + { + await next(); + } + }); + + return app; + } + + static bool ShouldSkipStatusCodeReExecution(string path) + { + return path.Contains("/api/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/_framework/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/_content/", StringComparison.OrdinalIgnoreCase) || + HasStaticFileExtension(path); + } + + static bool HasStaticFileExtension(string path) + { + var extension = Path.GetExtension(path); + if (string.IsNullOrEmpty(extension)) + { + return false; + } + + var staticExtensions = new[] { ".js", ".css", ".map", ".json", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp4", ".webm", ".ogg", ".mp3", ".wav", ".pdf", ".txt", ".xml" }; + return staticExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + } } } From 1279c30fbb93b9bf40da13916d13ce2717df2143 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 21 Nov 2025 18:34:22 +0800 Subject: [PATCH 13/48] Fix #5649: check path by internal api. --- .../Extensions/ApplicationBuilderExtensions.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index f3d11dc1..a18f4d02 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -159,7 +159,7 @@ namespace Oqtane.Extensions app.Use(async (context, next) => { var path = context.Request.Path.Value ?? string.Empty; - if (ShouldSkipStatusCodeReExecution(path)) + if (string.IsNullOrEmpty(path) || ShouldSkipStatusCodeReExecution(path)) { var feature = context.Features.Get(); feature?.Enabled = false; @@ -204,22 +204,12 @@ namespace Oqtane.Extensions static bool ShouldSkipStatusCodeReExecution(string path) { - return path.Contains("/api/", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("/_framework/", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("/_content/", StringComparison.OrdinalIgnoreCase) || - HasStaticFileExtension(path); + return Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) || HasStaticFileExtension(path); } static bool HasStaticFileExtension(string path) { - var extension = Path.GetExtension(path); - if (string.IsNullOrEmpty(extension)) - { - return false; - } - - var staticExtensions = new[] { ".js", ".css", ".map", ".json", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp4", ".webm", ".ogg", ".mp3", ".wav", ".pdf", ".txt", ".xml" }; - return staticExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + return !string.IsNullOrEmpty(Path.GetExtension(path)); } } } From 1e3c176ddf97044cf50e25e9975f54b860860f39 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 22 Nov 2025 13:31:04 +0100 Subject: [PATCH 14/48] Update ReplaceTokens on ModuleBase Check for Contents == null --- Oqtane.Client/Modules/ModuleBase.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 00dc2c61..264930c4 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -460,6 +460,11 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { + // check for null or empty content + if (string.IsNullOrEmpty(content)) + { + return content; + } // Using StringBuilder avoids the performance penalty of repeated string allocations // that occur with string.Replace or string concatenation inside loops. var sb = new StringBuilder(); From 0a04035b2fc37428892e01e7a3aad168ba443dbf Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Nov 2025 18:14:14 +0800 Subject: [PATCH 15/48] Fix #5836: update the setting by check existing first. --- Oqtane.Server/Controllers/PageController.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index cb7b8ee5..69bed0bc 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -265,7 +265,19 @@ namespace Oqtane.Controllers _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); // set user personalized page path - _settings.AddSetting(new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false }); + var settingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}"; + var pathSetting = _settings.GetSetting(EntityNames.User, page.UserId.Value, settingName); + if(pathSetting == null) + { + pathSetting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false }; + _settings.AddSetting(pathSetting); + } + else + { + pathSetting.SettingValue = path; + _settings.UpdateSetting(pathSetting); + } + _syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update); } } From 6ef6e6aac878baea8c41582b0e5fe4cf4af759ed Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Nov 2025 11:21:20 +0800 Subject: [PATCH 16/48] Fix #5839: do not send confirmation email to deleted users. --- Oqtane.Server/Managers/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 4e95d5f6..e6f2661d 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -279,7 +279,7 @@ namespace Oqtane.Managers await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated } - if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true"))) + if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")) && !user.IsDeleted) { if (user.EmailConfirmed) { From fb6e8bb23378b684f7fffa19c9cd6bfcc637bd32 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 25 Nov 2025 14:43:51 -0500 Subject: [PATCH 17/48] add Enhanced Navigation option in Site Settings --- Oqtane.Client/Modules/Admin/Site/Index.razor | 26 ++++++++++++----- Oqtane.Server/Components/App.razor | 4 ++- .../10000101_AddSiteEnhancedNavigation.cs | 29 +++++++++++++++++++ Oqtane.Shared/Models/Site.cs | 6 ++++ 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 525f45df..59e10be5 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -411,6 +411,18 @@ + @if (_rendermode == RenderModes.Static) + { +
+ +
+ +
+
+ }
@@ -537,6 +549,7 @@ private string _defaultalias; private string _rendermode = RenderModes.Interactive; + private string _enhancednavigation = "True"; private string _runtime = Runtimes.Server; private string _prerender = "True"; private string _hybrid = "False"; @@ -660,6 +673,7 @@ // hosting model _rendermode = site.RenderMode; + _enhancednavigation = site.EnhancedNavigation.ToString(); _runtime = site.Runtime; _prerender = site.Prerender.ToString(); _hybrid = site.Hybrid.ToString(); @@ -807,13 +821,11 @@ // hosting model if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - if (site.RenderMode != _rendermode || site.Runtime != _runtime || site.Prerender != bool.Parse(_prerender) || site.Hybrid != bool.Parse(_hybrid)) - { - site.RenderMode = _rendermode; - site.Runtime = _runtime; - site.Prerender = bool.Parse(_prerender); - site.Hybrid = bool.Parse(_hybrid); - } + site.RenderMode = _rendermode; + site.EnhancedNavigation = bool.Parse(_enhancednavigation); + site.Runtime = _runtime; + site.Prerender = bool.Parse(_prerender); + site.Hybrid = bool.Parse(_hybrid); } site = await SiteService.UpdateSiteAsync(site); diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 333ed42f..0082e669 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -60,7 +60,7 @@ } @((MarkupString)_headResources) - + @if (string.IsNullOrEmpty(_message)) { @if (_renderMode == RenderModes.Static) @@ -97,6 +97,7 @@ private string _renderMode = RenderModes.Interactive; private string _runtime = Runtimes.Server; private bool _prerender = true; + private bool _enhancedNavigation = true; private string _fingerprint = ""; private int _visitorId = -1; private string _antiForgeryToken = ""; @@ -141,6 +142,7 @@ _renderMode = site.RenderMode; _runtime = site.Runtime; _prerender = site.Prerender; + _enhancedNavigation = site.EnhancedNavigation; _fingerprint = site.Fingerprint; var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty); diff --git a/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs b/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs new file mode 100644 index 00000000..b119c2ee --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs @@ -0,0 +1,29 @@ +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.10.00.01.01")] + public class AddSiteEnhancedNavigation : MultiDatabaseMigration + { + public AddSiteEnhancedNavigation(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddBooleanColumn("EnhancedNavigation", true); + siteEntityBuilder.UpdateData("EnhancedNavigation", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index e674be2e..a00fa5b1 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -115,6 +115,11 @@ namespace Oqtane.Models /// public bool Hybrid { get; set; } + /// + /// Indicates if enhanced navigation should be used with static rendering + /// + public bool EnhancedNavigation { get; set; } + /// /// Keeps track of site configuration changes and is used by the ISiteMigration interface /// @@ -222,6 +227,7 @@ namespace Oqtane.Models Runtime = Runtime, Prerender = Prerender, Hybrid = Hybrid, + EnhancedNavigation = EnhancedNavigation, Version = Version, HomePageId = HomePageId, HeadContent = HeadContent, From 50c085fe652d9d9d23d236d958ec3114498231b1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 25 Nov 2025 14:49:45 -0500 Subject: [PATCH 18/48] update to .NET 10 PostgreSQL provider --- Oqtane.Package/Oqtane.Server.nuspec | 2 +- Oqtane.Server/Oqtane.Server.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 789f9215..6e4219a3 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -31,7 +31,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 1236dde0..d14efd87 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -43,7 +43,7 @@ - + From 321fe2954e3d7014b717e3babf2d1bff50eebab5 Mon Sep 17 00:00:00 2001 From: vnetonline Date: Sat, 29 Nov 2025 15:10:58 +1100 Subject: [PATCH 19/48] Added Style Paramater to RichTextEditor to remove the `margin-bottom: 50px;` if the developer wishes --- Oqtane.Client/Modules/Controls/RichTextEditor.razor | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index 6933783a..e4e98fba 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -7,7 +7,7 @@ @inject ISettingService SettingService @inject IStringLocalizer Localizer -
+
@_textEditorComponent
@@ -18,6 +18,8 @@ private RenderFragment _textEditorComponent; private ITextEditor _textEditor; + private string _style = "margin-bottom: 50px;"; + [Parameter] public string Content { get; set; } @@ -30,6 +32,9 @@ [Parameter] public string Provider { get; set; } + [Parameter] + public string Style { get; set; } // optional + [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } = new Dictionary(); @@ -40,6 +45,12 @@ protected override void OnParametersSet() { + + if (!string.IsNullOrEmpty(Style)) + { + _style = Style; + } + _textEditorComponent = (builder) => { CreateTextEditor(builder); From 171f9c84a00ea8d55446e6626bc9bfca1fdbae84 Mon Sep 17 00:00:00 2001 From: Jon Welfringer <7365166+W6HBR@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:36:19 -0800 Subject: [PATCH 20/48] Fix SMTPRelay condition for sender email validation Prior change was leaving sender null and not properly setting "From" address when used in a relay configuration. This caused emails to go to the deleted state and not be delivered. --- 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 9d320bd9..a280c71f 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -186,7 +186,7 @@ namespace Oqtane.Infrastructure var mailboxAddressValidationError = ""; // sender - if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") != "True") + if ((settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True") && string.IsNullOrEmpty(fromEmail)) { fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", ""); fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName; From cf88347c3d4427108b010698b805332e1f2f13c9 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 2 Dec 2025 09:23:43 +0800 Subject: [PATCH 21/48] Fix #5849: correct resources key. --- Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx index 0e212ab8..fbb57778 100644 --- a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx @@ -204,7 +204,7 @@ Page Deleted Successfully - + All Pages Deleted Successfully From 86a3f67871df47993c77df4fa8dbb123affbdc36 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 3 Dec 2025 09:08:01 +0800 Subject: [PATCH 22/48] Fix #5852: clear the cache after import content. --- .../HtmlText/Manager/HtmlTextManager.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index bca9f928..a60868be 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Modules.HtmlText.Repository; @@ -12,6 +15,7 @@ using Oqtane.Interfaces; using System.Collections.Generic; using System; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; // ReSharper disable ConvertToUsingDeclaration @@ -23,15 +27,21 @@ namespace Oqtane.Modules.HtmlText.Manager private readonly IHtmlTextRepository _htmlText; private readonly IDBContextDependencies _DBContextDependencies; private readonly ISqlRepository _sqlRepository; + private readonly ITenantManager _tenantManager; + private readonly IMemoryCache _cache; public HtmlTextManager( IHtmlTextRepository htmlText, IDBContextDependencies DBContextDependencies, - ISqlRepository sqlRepository) + ISqlRepository sqlRepository, + ITenantManager tenantManager, + IMemoryCache cache) { _htmlText = htmlText; _DBContextDependencies = DBContextDependencies; _sqlRepository = sqlRepository; + _tenantManager = tenantManager; + _cache = cache; } public string ExportModule(Module module) @@ -71,6 +81,13 @@ namespace Oqtane.Modules.HtmlText.Manager htmlText.ModuleId = module.ModuleId; htmlText.Content = content; _htmlText.AddHtmlText(htmlText); + + //clear the cache for the module + var alias = _tenantManager.GetAlias(); + if(alias != null) + { + _cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}"); + } } public bool Install(Tenant tenant, string version) From 47f42747cb1e46f49df2b84560951eb27b2ba9cf Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 3 Dec 2025 09:09:43 +0800 Subject: [PATCH 23/48] clean the usage. --- Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index a60868be..67f05805 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -10,10 +10,7 @@ using Oqtane.Repository; using Oqtane.Shared; using Oqtane.Migrations.Framework; using Oqtane.Documentation; -using System.Linq; using Oqtane.Interfaces; -using System.Collections.Generic; -using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; From 270b447fbd348fdbd61ee58eef9756b4f3b7f9da Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 3 Dec 2025 11:19:10 +0100 Subject: [PATCH 24/48] Package updates Radzen, Swashbuckle Added the Bold tool to the Radzen editor. --- .../Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs | 1 + Oqtane.Client/Oqtane.Client.csproj | 2 +- Oqtane.Package/Oqtane.Client.nuspec | 2 +- Oqtane.Package/Oqtane.Server.nuspec | 2 +- Oqtane.Server/Oqtane.Server.csproj | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs index 2326bae7..0f483562 100644 --- a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs +++ b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs @@ -31,6 +31,7 @@ namespace Oqtane.Modules.Controls { "FormatBlock", (builder, sequence) => CreateFragment(builder, sequence, "FormatBlock", "RadzenHtmlEditorFormatBlock") }, { "Indent", (builder, sequence) => CreateFragment(builder, sequence, "Indent", "RadzenHtmlEditorIndent") }, { "InsertImage", (builder, sequence) => CreateFragment(builder, sequence, "InsertImage", "RadzenHtmlEditorCustomTool", "InsertImage", "image") }, + { "Bold", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorBold") }, { "Italic", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorItalic") }, { "Justify", (builder, sequence) => CreateFragment(builder, sequence, "Justify", "RadzenHtmlEditorJustify") }, { "Link", (builder, sequence) => CreateFragment(builder, sequence, "InsertLink", "RadzenHtmlEditorCustomTool", "InsertLink", "insert_link") }, diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index bcc69ef0..e3213314 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -12,7 +12,7 @@ - + diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 6fea68b2..ba1bd798 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -23,7 +23,7 @@ - + diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 6e4219a3..3257a474 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -26,7 +26,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index d14efd87..a3498029 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -33,7 +33,7 @@ - + From 23d14c62a50e3accf40832190ec2f1d8ee4b145a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 3 Dec 2025 15:28:31 -0500 Subject: [PATCH 25/48] remove unique index of TenantId and Name from Site table as site name does not need to be unique. Remove TenantId column from Site table as it is not necessary and should be obtained from the Alias. --- Oqtane.Client/Modules/Admin/Site/Index.razor | 12 ++++---- .../Modules/Admin/SystemInfo/Index.razor | 2 +- .../Infrastructure/DatabaseManager.cs | 4 +-- .../Tenant/10000102_RemoveSiteTenantId.cs | 29 +++++++++++++++++++ Oqtane.Server/Services/SiteService.cs | 4 ++- Oqtane.Shared/Models/Site.cs | 15 +++++----- 6 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 59e10be5..0ceec1fd 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -683,7 +683,7 @@ { var tenants = await TenantService.GetTenantsAsync(); var _databases = await DatabaseService.GetDatabasesAsync(); - var tenant = tenants.Find(item => item.TenantId == site.TenantId); + var tenant = tenants.Find(item => item.TenantId == PageState.Alias.TenantId); if (tenant != null) { _tenant = tenant.Name; @@ -886,17 +886,17 @@ try { var aliases = await AliasService.GetAliasesAsync(); - if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId)) + if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Alias.TenantId)) { await SiteService.DeleteSiteAsync(PageState.Site.SiteId); await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId); - foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId)) + foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId)) { await AliasService.DeleteAliasAsync(alias.AliasId); } - var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId); + var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Alias.TenantId); NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + redirect.Name, true); } else @@ -993,7 +993,7 @@ if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { _aliases = await AliasService.GetAliasesAsync(); - _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList(); + _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); } } @@ -1046,7 +1046,7 @@ { if (_aliasid == 0) { - alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; + alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; await AliasService.AddAliasAsync(alias); } else diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index 6f7c4b02..c64070ff 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -271,7 +271,7 @@ } var tenants = await TenantService.GetTenantsAsync(); - _tenant = tenants.Find(item => item.TenantId == PageState.Site.TenantId).Name; + _tenant = tenants.Find(item => item.TenantId == PageState.Alias.TenantId).Name; _history = await MigrationHistoryService.GetMigrationHistoryAsync(); _initialized = true; diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 1d4debe7..afad9465 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -579,7 +579,6 @@ namespace Oqtane.Infrastructure site = new Site { - TenantId = tenant.TenantId, Name = install.SiteName, LogoFileId = null, FaviconFileId = null, @@ -596,7 +595,8 @@ namespace Oqtane.Infrastructure RenderMode = rendermode, Runtime = runtime, Prerender = (rendermode == RenderModes.Interactive), - Hybrid = false + Hybrid = false, + TenantId = tenant.TenantId }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs b/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs new file mode 100644 index 00000000..3ac75cbe --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs @@ -0,0 +1,29 @@ +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.10.00.01.02")] + public class RemoveSiteTenantId : MultiDatabaseMigration + { + public RemoveSiteTenantId(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.DropIndex("IX_Site"); // TenantId, Name + siteEntityBuilder.DropColumn("TenantId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 25a9ec92..5e43299b 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -148,6 +148,8 @@ namespace Oqtane.Services // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); + + site.TenantId = alias.TenantId; } else { @@ -181,7 +183,7 @@ namespace Oqtane.Services { var alias = _tenantManager.GetAlias(); var current = _sites.GetSite(site.SiteId, false); - if (site.SiteId == alias.SiteId && site.TenantId == alias.TenantId && current != null) + if (site.SiteId == alias.SiteId && current != null) { site = _sites.UpdateSite(site); _syncManager.AddSyncEvent(alias, EntityNames.Site, site.SiteId, SyncEventActions.Update); diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index a00fa5b1..2b2cb78b 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -16,11 +16,6 @@ namespace Oqtane.Models /// public int SiteId { get; set; } - /// - /// Reference to the the Site is in - /// - public int TenantId { get; set; } - /// /// The site Name /// @@ -203,12 +198,17 @@ namespace Oqtane.Models [NotMapped] public string Fingerprint { get; set; } + /// + /// Reference to the the Site belongs to + /// + [NotMapped] + public int TenantId { get; set; } + public Site Clone() { return new Site { SiteId = SiteId, - TenantId = TenantId, Name = Name, TimeZoneId = TimeZoneId, LogoFileId = LogoFileId, @@ -246,7 +246,8 @@ namespace Oqtane.Models Pages = Pages.ConvertAll(page => page.Clone()), Languages = Languages.ConvertAll(language => language.Clone()), Themes = Themes, - Fingerprint = Fingerprint + Fingerprint = Fingerprint, + TenantId = TenantId }; } From a51f87d743eb88b153a2357e20870b1c773c7570 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 5 Dec 2025 08:40:30 -0500 Subject: [PATCH 26/48] move user workload from siterouter to app component to improve performance and 404 handling --- Oqtane.Client/UI/PageState.cs | 5 +-- Oqtane.Client/UI/SiteRouter.razor | 17 +++++----- Oqtane.Client/_Imports.razor | 1 + Oqtane.Server/Components/App.razor | 12 ++++--- .../Options/ISiteNamedOptions.cs | 11 ------- .../Infrastructure/Options/ISiteOptions.cs | 11 ------- .../Options/SiteNamedOptions.cs | 6 ++++ .../Infrastructure/Options/SiteOptions.cs | 6 ++++ Oqtane.Server/Services/SiteService.cs | 11 ++++++- .../Extensions/ClaimsPrincipalExtensions.cs | 12 +++++++ Oqtane.Shared/Models/Site.cs | 7 +++++ Oqtane.Shared/Models/User.cs | 31 +++++++++++++++++++ 12 files changed, 91 insertions(+), 39 deletions(-) delete mode 100644 Oqtane.Server/Infrastructure/Options/ISiteNamedOptions.cs delete mode 100644 Oqtane.Server/Infrastructure/Options/ISiteOptions.cs rename {Oqtane.Server => Oqtane.Shared}/Extensions/ClaimsPrincipalExtensions.cs (91%) diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs index 8aac9627..91cf158c 100644 --- a/Oqtane.Client/UI/PageState.cs +++ b/Oqtane.Client/UI/PageState.cs @@ -29,8 +29,6 @@ namespace Oqtane.UI public bool Refresh { get; set; } public bool AllowCookies { get; set; } - public int? StatusCode { get; set; } - public List Pages { get { return Site?.Pages; } @@ -65,8 +63,7 @@ namespace Oqtane.UI IsInternalNavigation = IsInternalNavigation, RenderId = RenderId, Refresh = Refresh, - AllowCookies = AllowCookies, - StatusCode = StatusCode + AllowCookies = AllowCookies }; } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index e7b11256..fb7d4fc3 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -158,13 +158,17 @@ // verify user is authenticated for current site var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - if (authState.User.Identity.IsAuthenticated - && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey) - && PageState.StatusCode != (int)HttpStatusCode.NotFound) + if (authState.User.IsAuthenticated() && authState.User.SiteKey() == SiteState.Alias.SiteKey) { - // get user - var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value); - user = await UserService.GetUserAsync(userid, SiteState.Alias.SiteId); + if (PageState == null || PageState.User == null || PageState.User.UserId != authState.User.UserId()) + { + // get user + user = await UserService.GetUserAsync(authState.User.UserId(), SiteState.Alias.SiteId); + } + else + { + user = PageState.User; + } if (user != null) { user.IsAuthenticated = authState.User.Identity.IsAuthenticated; @@ -339,7 +343,6 @@ IsInternalNavigation = _isInternalNavigation, RenderId = renderid, Refresh = false, - StatusCode = PageState?.StatusCode, AllowCookies = _allowCookies.GetValueOrDefault(true) }; OnStateChange?.Invoke(_pagestate); diff --git a/Oqtane.Client/_Imports.razor b/Oqtane.Client/_Imports.razor index 73af8f3a..86d19622 100644 --- a/Oqtane.Client/_Imports.razor +++ b/Oqtane.Client/_Imports.razor @@ -27,3 +27,4 @@ @using Oqtane.Enums @using Oqtane.Installer @using Oqtane.Interfaces +@using Oqtane.Extensions diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 0082e669..6276eea0 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -60,7 +60,7 @@ } @((MarkupString)_headResources) - + @if (string.IsNullOrEmpty(_message)) { @if (_renderMode == RenderModes.Static) @@ -97,7 +97,7 @@ private string _renderMode = RenderModes.Interactive; private string _runtime = Runtimes.Server; private bool _prerender = true; - private bool _enhancedNavigation = true; + Dictionary _bodyAttributes { get; set; } = new(); private string _fingerprint = ""; private int _visitorId = -1; private string _antiForgeryToken = ""; @@ -142,7 +142,10 @@ _renderMode = site.RenderMode; _runtime = site.Runtime; _prerender = site.Prerender; - _enhancedNavigation = site.EnhancedNavigation; + if (_renderMode == RenderModes.Static && !site.EnhancedNavigation) + { + _bodyAttributes.Add("data-enhance-nav", "false"); + } _fingerprint = site.Fingerprint; var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty); @@ -234,7 +237,7 @@ Site = site, Page = page, Modules = modules, - User = null, + User = site.User, Uri = new Uri(url, UriKind.Absolute), Route = route, QueryString = Utilities.ParseQueryString(route.Query), @@ -251,7 +254,6 @@ IsInternalNavigation = false, RenderId = Guid.NewGuid(), Refresh = true, - StatusCode = Context.Response.StatusCode, AllowCookies = _allowCookies }; } diff --git a/Oqtane.Server/Infrastructure/Options/ISiteNamedOptions.cs b/Oqtane.Server/Infrastructure/Options/ISiteNamedOptions.cs deleted file mode 100644 index 41ed97c4..00000000 --- a/Oqtane.Server/Infrastructure/Options/ISiteNamedOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Models; - -namespace Oqtane.Infrastructure -{ - public interface ISiteNamedOptions - where TOptions : class, new() - { - void Configure(string name, TOptions options, Alias alias, Dictionary sitesettings); - } -} diff --git a/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs b/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs deleted file mode 100644 index c05cd9fa..00000000 --- a/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Models; - -namespace Oqtane.Infrastructure -{ - public interface ISiteOptions - where TOptions : class, new() - { - void Configure(TOptions options, Alias alias, Dictionary sitesettings); - } -} diff --git a/Oqtane.Server/Infrastructure/Options/SiteNamedOptions.cs b/Oqtane.Server/Infrastructure/Options/SiteNamedOptions.cs index 1027cc54..0ebe5a91 100644 --- a/Oqtane.Server/Infrastructure/Options/SiteNamedOptions.cs +++ b/Oqtane.Server/Infrastructure/Options/SiteNamedOptions.cs @@ -4,6 +4,12 @@ using Oqtane.Models; namespace Oqtane.Infrastructure { + public interface ISiteNamedOptions + where TOptions : class, new() + { + void Configure(string name, TOptions options, Alias alias, Dictionary sitesettings); + } + public class SiteNamedOptions : ISiteNamedOptions where TOptions : class, new() { diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptions.cs b/Oqtane.Server/Infrastructure/Options/SiteOptions.cs index f4f55f77..e5d4e7c0 100644 --- a/Oqtane.Server/Infrastructure/Options/SiteOptions.cs +++ b/Oqtane.Server/Infrastructure/Options/SiteOptions.cs @@ -4,6 +4,12 @@ using Oqtane.Models; namespace Oqtane.Infrastructure { + public interface ISiteOptions + where TOptions : class, new() + { + void Configure(TOptions options, Alias alias, Dictionary sitesettings); + } + public class SiteOptions : ISiteOptions where TOptions : class, new() { diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 5e43299b..14b28c2a 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -13,6 +13,7 @@ using Oqtane.Enums; using Oqtane.Shared; using System.Globalization; using Oqtane.Extensions; +using Oqtane.Managers; namespace Oqtane.Services { @@ -25,6 +26,7 @@ namespace Oqtane.Services private readonly IPageModuleRepository _pageModules; private readonly IModuleDefinitionRepository _moduleDefinitions; private readonly ILanguageRepository _languages; + private readonly IUserManager _userManager; private readonly IUserPermissions _userPermissions; private readonly ISettingRepository _settings; private readonly ITenantManager _tenantManager; @@ -35,7 +37,7 @@ namespace Oqtane.Services private readonly IHttpContextAccessor _accessor; private readonly string _private = "[PRIVATE]"; - public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) + public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) { _sites = sites; _pages = pages; @@ -43,6 +45,7 @@ namespace Oqtane.Services _pageModules = pageModules; _moduleDefinitions = moduleDefinitions; _languages = languages; + _userManager = userManager; _userPermissions = userPermissions; _settings = settings; _tenantManager = tenantManager; @@ -146,6 +149,12 @@ namespace Oqtane.Services // themes site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList()); + // user + if (_accessor.HttpContext.User.IsAuthenticated()) + { + site.User = _userManager.GetUser(_accessor.HttpContext.User.UserId(), site.SiteId); + } + // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); diff --git a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs b/Oqtane.Shared/Extensions/ClaimsPrincipalExtensions.cs similarity index 91% rename from Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs rename to Oqtane.Shared/Extensions/ClaimsPrincipalExtensions.cs index 1749c421..2adfbcde 100644 --- a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs +++ b/Oqtane.Shared/Extensions/ClaimsPrincipalExtensions.cs @@ -8,6 +8,18 @@ namespace Oqtane.Extensions { // extension methods cannot be properties - the methods below must include a () suffix when referenced + public static bool IsAuthenticated(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.Identity != null) + { + return claimsPrincipal.Identity.IsAuthenticated; + } + else + { + return false; + } + } + public static string Username(this ClaimsPrincipal claimsPrincipal) { if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name)) diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 2b2cb78b..9b444671 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -192,6 +192,12 @@ namespace Oqtane.Models [NotMapped] public List Themes { get; set; } + /// + /// Current user + /// + [NotMapped] + public User User { get; set; } + /// /// fingerprint for framework static assets /// @@ -246,6 +252,7 @@ namespace Oqtane.Models Pages = Pages.ConvertAll(page => page.Clone()), Languages = Languages.ConvertAll(language => language.Clone()), Themes = Themes, + User = User.Clone(), Fingerprint = Fingerprint, TenantId = TenantId }; diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index 7b6b398b..dd3fa320 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Xml.Linq; namespace Oqtane.Models { @@ -128,5 +130,34 @@ namespace Oqtane.Models /// [NotMapped] public Dictionary Settings { get; set; } + + public User Clone() + { + return new User + { + UserId = UserId, + Username = Username, + DisplayName = DisplayName, + Email = Email, + TimeZoneId = TimeZoneId, + PhotoFileId = PhotoFileId, + LastLoginOn = LastLoginOn, + LastIPAddress = LastIPAddress, + TwoFactorRequired = TwoFactorRequired, + TwoFactorCode = TwoFactorCode, + TwoFactorExpiry = TwoFactorExpiry, + SecurityStamp = SecurityStamp, + SiteId = SiteId, + Roles = Roles, + DeletedBy = DeletedBy, + DeletedOn = DeletedOn, + IsDeleted = IsDeleted, + Password = Password, + IsAuthenticated = IsAuthenticated, + EmailConfirmed = EmailConfirmed, + SuppressNotification = SuppressNotification, + Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value) + }; + } } } From d7c0b0aaafda253e439a3f6d0e637f4f63f7f674 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 11 Dec 2025 15:08:52 -0500 Subject: [PATCH 27/48] update to .NET SDK 10.0.1 --- .../Client/Oqtane.Application.Client.csproj | 8 ++++---- .../Server/Oqtane.Application.Server.csproj | 6 +++--- Oqtane.Client/Oqtane.Client.csproj | 10 +++++----- Oqtane.Maui/Oqtane.Maui.csproj | 10 +++++----- Oqtane.Server/Oqtane.Server.csproj | 14 +++++++------- .../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 | 6 +++--- 9 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index fdbfd650..ff040e3b 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 4d6ad43b..0b4fb365 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -22,9 +22,9 @@ - - - + + + diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index e3213314..aca8f7ae 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 2d4b3572..cad6792a 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -54,11 +54,11 @@ - - - - - + + + + + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index a3498029..1f282058 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -27,10 +27,10 @@ - - - - + + + + @@ -45,10 +45,10 @@ - - + + - + 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 607195bc..2afa3718 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 4e70b5b0..95f39be0 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 @@ -20,10 +20,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 8333d87e..ca8517c6 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 @@ -14,9 +14,9 @@ - - - + + + diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index c1bdff46..8f7ddde6 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -5,9 +5,9 @@ - - - + + + From 156e7bd3d451fc951814098125603292642f656b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 11 Dec 2025 15:13:45 -0500 Subject: [PATCH 28/48] update nuspec files to .NET SDK 10.0.1 --- Oqtane.Package/Oqtane.Client.nuspec | 10 +++++----- Oqtane.Package/Oqtane.Server.nuspec | 18 +++++++++--------- Oqtane.Package/Oqtane.Shared.nuspec | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index ba1bd798..4b779809 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -19,11 +19,11 @@ - - - - - + + + + + diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 3257a474..52a1d351 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -18,12 +18,12 @@ oqtane - - - - - - + + + + + + @@ -32,9 +32,9 @@ - - - + + + diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 9e68e8ca..a70d126b 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -18,9 +18,9 @@ oqtane - - - + + + From 53a88e0c9fd38e5c902a37a2acddccb62bec6bb8 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 11 Dec 2025 15:26:42 -0500 Subject: [PATCH 29/48] bump Oqtane version to 10.0.1 --- Directory.Build.props | 4 ++-- Oqtane.Application/Client/Oqtane.Application.Client.csproj | 2 +- Oqtane.Application/Server/Oqtane.Application.Server.csproj | 2 +- Oqtane.Application/Shared/Oqtane.Application.Shared.csproj | 2 +- Oqtane.Maui/Oqtane.Maui.csproj | 2 +- 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.Shared/Shared/Constants.cs | 4 ++-- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index aff5bd3e..28379aa7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net10.0 Debug;Release - 10.0.0 + 10.0.1 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/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 https://github.com/oqtane/oqtane.framework Git diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index ff040e3b..d932bbb6 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -23,7 +23,7 @@ - + diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 0b4fb365..c1ed4362 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj index bc957313..a063b58e 100644 --- a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj +++ b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj @@ -11,7 +11,7 @@ - + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index cad6792a..2314ac0a 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -18,7 +18,7 @@ com.oqtane.maui - 10.0.0 + 10.0.1 1 diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 4b779809..de21e194 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 10.0.0 + 10.0.1 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/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 2a1dabff..952bb267 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 10.0.0 + 10.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v10.0.0/Oqtane.Framework.10.0.0.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/download/v10.0.1/Oqtane.Framework.10.0.1.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 52a1d351..47a0b13a 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 10.0.0 + 10.0.1 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/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index a70d126b..78dfbf9d 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 10.0.0 + 10.0.1 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/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 597f5617..6873d041 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 10.0.0 + 10.0.1 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/v10.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index a59e4412..6efebb86 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.0.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.1.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 5979f4d7..4f20bec7 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.0.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.1.Upgrade.zip" -Force diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index d6740658..3189ccce 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 = "10.0.0"; - 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,6.1.5,6.2.0,6.2.1,10.0.0"; + public static readonly string Version = "10.0.1"; + 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,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; From f459d0503a541de11cf12f08fbc658a350801b9c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 11 Dec 2025 15:51:27 -0500 Subject: [PATCH 30/48] update version in Oqtane Application Template nuspec --- Oqtane.Application/Oqtane.Application.Template.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Application/Oqtane.Application.Template.nuspec b/Oqtane.Application/Oqtane.Application.Template.nuspec index e7097801..1b9a140b 100644 --- a/Oqtane.Application/Oqtane.Application.Template.nuspec +++ b/Oqtane.Application/Oqtane.Application.Template.nuspec @@ -2,7 +2,7 @@ Oqtane.Application.Template - 10.0.0 + 10.0.1 Oqtane Application Template For Blazor Shaun Walker false From 011375a081c6f9771950a37e840d7013ff7a0cbb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 11 Dec 2025 19:28:50 -0500 Subject: [PATCH 31/48] admin dashboard should always use enhanced navigation --- Oqtane.Client/Modules/Admin/Dashboard/Index.razor | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor index 9ca81674..c04f97e8 100644 --- a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor +++ b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor @@ -13,7 +13,7 @@ { string url = NavigateUrl(p.Path);
- +

@((MarkupString)SharedLocalizer[p.Name].ToString().Replace(" ", "
"))
@@ -24,13 +24,19 @@ } @code { - private List _pages; + Dictionary _attributes { get; set; } = new(); + private List _pages; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override string RenderMode => RenderModes.Static; protected override void OnInitialized() { + if (PageState.RenderMode == RenderModes.Static && !PageState.Site.EnhancedNavigation) + { + _attributes.Add("data-enhance-nav", "true"); // Admin Dashboard utilizes enhanced navigation + } + var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin"); if (admin != null) { From 6b883b3f948c57c7435c4afef6524ab033930309 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 12 Dec 2025 15:56:50 -0500 Subject: [PATCH 32/48] add null check for User --- Oqtane.Shared/Models/Site.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 9b444671..4ba4ede5 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -252,7 +252,7 @@ namespace Oqtane.Models Pages = Pages.ConvertAll(page => page.Clone()), Languages = Languages.ConvertAll(language => language.Clone()), Themes = Themes, - User = User.Clone(), + User = User?.Clone(), Fingerprint = Fingerprint, TenantId = TenantId }; From a0e45cbea0713b1889f79d81c70a0da8c32682d6 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 13 Dec 2025 12:55:24 +0100 Subject: [PATCH 33/48] Add null checks for RenderModeBoundary in ModuleBase methods Add null checks to key ModuleBase methods to ensure RenderModeBoundary is available before use. Throw a detailed InvalidOperationException with guidance if it is missing, improving error handling and developer feedback. --- Oqtane.Client/Modules/ModuleBase.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 264930c4..43851818 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -372,6 +372,11 @@ namespace Oqtane.Modules } // UI methods + private static readonly string RenderModeBoundaryErrorMessage = + "RenderModeBoundary is not available. This method requires a RenderModeBoundary parameter. " + + "If you are using child components, ensure you pass the RenderModeBoundary property to the child component: " + + ""; + public void AddModuleMessage(string message, MessageType type) { AddModuleMessage(message, type, "top"); @@ -389,21 +394,37 @@ namespace Oqtane.Modules public void AddModuleMessage(string message, MessageType type, string position, MessageStyle style) { + if (RenderModeBoundary == null) + { + throw new InvalidOperationException(RenderModeBoundaryErrorMessage); + } RenderModeBoundary.AddModuleMessage(message, type, position, style); } public void ClearModuleMessage() { + if (RenderModeBoundary == null) + { + throw new InvalidOperationException(RenderModeBoundaryErrorMessage); + } RenderModeBoundary.AddModuleMessage("", MessageType.Undefined); } public void ShowProgressIndicator() { + if (RenderModeBoundary == null) + { + throw new InvalidOperationException(RenderModeBoundaryErrorMessage); + } RenderModeBoundary.ShowProgressIndicator(); } public void HideProgressIndicator() { + if (RenderModeBoundary == null) + { + throw new InvalidOperationException(RenderModeBoundaryErrorMessage); + } RenderModeBoundary.HideProgressIndicator(); } From a33e9d25cc989a84d931d19807785be2476410c9 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 13 Dec 2025 13:04:30 +0100 Subject: [PATCH 34/48] Add AltText/title support to ActionDialog and ActionLink Introduce optional AltText parameter to ActionDialog and ActionLink components. AltText is now used as the title attribute on rendered buttons and links, providing tooltips for improved accessibility and user experience. All relevant elements, including those in disabled states, now support this enhancement. --- Oqtane.Client/Modules/Controls/ActionDialog.razor | 11 +++++++---- Oqtane.Client/Modules/Controls/ActionLink.razor | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index 83948efe..11204bc7 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -35,11 +35,11 @@ { if (Disabled) { - + } else { - + } } } @@ -83,13 +83,13 @@ else { if (Disabled) { - + } else {
- +
} } @@ -112,6 +112,9 @@ else [Parameter] public string Text { get; set; } // optional - defaults to Action if not specified + + [Parameter] + public string AltText { get; set; } // optional [Parameter] public string Action { get; set; } // optional diff --git a/Oqtane.Client/Modules/Controls/ActionLink.razor b/Oqtane.Client/Modules/Controls/ActionLink.razor index 545dca5c..576a040f 100644 --- a/Oqtane.Client/Modules/Controls/ActionLink.razor +++ b/Oqtane.Client/Modules/Controls/ActionLink.razor @@ -8,17 +8,17 @@ { if (Disabled) { - @((MarkupString)_iconSpan) @_text + @((MarkupString)_iconSpan) @_text } else { if (OnClick == null) { - @((MarkupString)_iconSpan) @_text + @((MarkupString)_iconSpan) @_text } else { - + } } } @@ -42,6 +42,9 @@ [Parameter] public string Text { get; set; } // optional - defaults to Action if not specified + [Parameter] + public string AltText { get; set; } // optional + [Parameter] public int ModuleId { get; set; } = -1; // optional - allows the link to target a specific moduleid From 01ad99b92589161cbd6633d08236e499cf85ee7a Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 13 Dec 2025 18:13:37 +0100 Subject: [PATCH 35/48] Enhance tab authorization with role and permission checks #5872 Add RoleName and PermissionName parameters to TabPanel for fine-grained tab visibility control. Update IsAuthorized logic in TabStrip to prioritize Host/Admin access, then check SecurityAccessLevel, and additionally require specified roles or permissions if provided. Removes redundant Admin/Host checks from the switch statement for clarity. --- Oqtane.Client/Modules/Controls/TabPanel.razor | 6 +++ Oqtane.Client/Modules/Controls/TabStrip.razor | 40 +++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/TabPanel.razor b/Oqtane.Client/Modules/Controls/TabPanel.razor index cff8d9e1..5ac41365 100644 --- a/Oqtane.Client/Modules/Controls/TabPanel.razor +++ b/Oqtane.Client/Modules/Controls/TabPanel.razor @@ -30,6 +30,12 @@ else [Parameter] public SecurityAccessLevel? Security { get; set; } // optional - can be used to specify SecurityAccessLevel + [Parameter] + public string RoleName { get; set; } // optional - can be used to specify Role allowed to view this tab + + [Parameter] + public string PermissionName { get; set; } // optional - can be used to specify Permission allowed to view this tab + protected override void OnParametersSet() { base.OnParametersSet(); diff --git a/Oqtane.Client/Modules/Controls/TabStrip.razor b/Oqtane.Client/Modules/Controls/TabStrip.razor index e2a3c0f1..8fc5b2c7 100644 --- a/Oqtane.Client/Modules/Controls/TabStrip.razor +++ b/Oqtane.Client/Modules/Controls/TabStrip.razor @@ -84,12 +84,31 @@ } } + /// + /// Determines if a tab should be visible based on user permissions. + /// Authorization hierarchy: + /// 1. Host and Admin roles ALWAYS have access (bypass all checks) + /// 2. Check standard SecurityAccessLevel (View, Edit, etc.) + /// 3. If RoleName specified AND user is not Admin/Host, check RoleName + /// 4. If PermissionName specified AND user is not Admin/Host, check PermissionName + /// + /// The tab panel to check authorization for + /// True if user is authorized to see this tab, false otherwise private bool IsAuthorized(TabPanel tabPanel) { + // Step 1: Host and Admin bypass all restrictions + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) || + UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) + { + return true; + } + var authorized = false; + + // Step 2: Check standard SecurityAccessLevel switch (tabPanel.Security) { - case null: // security not specified - assume SecurityAccessLevel.Anonymous + case null: authorized = true; break; case SecurityAccessLevel.Anonymous: @@ -101,13 +120,20 @@ case SecurityAccessLevel.Edit: authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList); break; - case SecurityAccessLevel.Admin: - authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin); - break; - case SecurityAccessLevel.Host: - authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); - break; } + + // Step 3: Check RoleName if provided (additional requirement) + if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName)) + { + authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName); + } + + // Step 4: Check PermissionName if provided (additional requirement) + if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName)) + { + authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList); + } + return authorized; } } From e62268af2e3ba72fd2eed121c8284d61d2cb3931 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sat, 13 Dec 2025 21:56:05 +0100 Subject: [PATCH 36/48] Update TabStrip.razor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authorization flow is: • Host tabs: Only Host (Admin blocked by Step 1) • Everything else: Admin bypasses, others check permissions --- Oqtane.Client/Modules/Controls/TabStrip.razor | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/TabStrip.razor b/Oqtane.Client/Modules/Controls/TabStrip.razor index 8fc5b2c7..a8402d86 100644 --- a/Oqtane.Client/Modules/Controls/TabStrip.razor +++ b/Oqtane.Client/Modules/Controls/TabStrip.razor @@ -96,16 +96,22 @@ /// True if user is authorized to see this tab, false otherwise private bool IsAuthorized(TabPanel tabPanel) { - // Step 1: Host and Admin bypass all restrictions - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) || - UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) + // Step 1: Check for Host-only restriction + if (tabPanel.Security == SecurityAccessLevel.Host) + { + // Only Host users can access Host-level security tabs (Admin users are excluded) + return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); + } + + // Step 2: Admin bypass all other restrictions + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { return true; } var authorized = false; - // Step 2: Check standard SecurityAccessLevel + // Step 3: Check standard SecurityAccessLevel switch (tabPanel.Security) { case null: @@ -120,15 +126,18 @@ case SecurityAccessLevel.Edit: authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList); break; + case SecurityAccessLevel.Host: + authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); + break; } - // Step 3: Check RoleName if provided (additional requirement) + // Step 4: Check RoleName if provided (additional requirement) if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName)) { authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName); } - // Step 4: Check PermissionName if provided (additional requirement) + // Step 5: Check PermissionName if provided (additional requirement) if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName)) { authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList); From ec2afd5f030bab3b4818d0943e642470079e90ef Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 14 Dec 2025 15:13:53 -0500 Subject: [PATCH 37/48] added support for Forgot Username and Use Login Link --- Oqtane.Client/Modules/Admin/Login/Index.razor | 316 ++++++++++++------ Oqtane.Client/Modules/Admin/Users/Index.razor | 12 + .../Resources/Modules/Admin/Login/Index.resx | 47 ++- .../Resources/Modules/Admin/Users/Index.resx | 6 + Oqtane.Client/Services/UserService.cs | 30 +- Oqtane.Server/Controllers/UserController.cs | 27 +- Oqtane.Server/Managers/UserManager.cs | 99 +++++- Oqtane.Server/Pages/LoginLink.cshtml | 3 + Oqtane.Server/Pages/LoginLink.cshtml.cs | 69 ++++ .../Resources/Managers/UserManager.resx | 14 +- Oqtane.Shared/Shared/ExternalLoginStatus.cs | 1 + 11 files changed, 500 insertions(+), 124 deletions(-) create mode 100644 Oqtane.Server/Pages/LoginLink.cshtml create mode 100644 Oqtane.Server/Pages/LoginLink.cshtml.cs diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 83d49fd9..460aceab 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -14,93 +14,133 @@ } else { - @if (!twofactor) - { -
-
+
+ +
+ +
+
@@ -547,6 +556,7 @@ else private string _registerurl; private string _profileurl; private string _requireconfirmedemail; + private string _loginlink; private string _passkeys; private string _twofactor; private string _cookiename; @@ -625,6 +635,7 @@ else _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true"); + _loginlink = SettingService.GetSetting(settings, "LoginOptions:LoginLink", "false"); _passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false"); _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application"); @@ -764,6 +775,7 @@ else settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false); + settings = SettingService.SetSetting(settings, "LoginOptions:LoginLink", _loginlink, false); settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 80f587e9..af2a52ba 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -120,6 +120,12 @@ Forgot Password? + + Forgot Username? + + + Use Login Link + User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password. @@ -142,16 +148,19 @@ You Are Already Signed In - Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again - - Please Check The Email Address Associated To Your User Account For A Password Reset Notification + + Please Check Your Email For A Username Reminder Notification + + + A Login Link Has Been Sent To Your Email Address. The Link Is Only Valid For A Limited Amount Of Time. + - User Does Not Exist + User Does Not Exist For Criteria Specified - Please Enter The Secure Verification Code Which Was Sent To You By Email. + Please enter the secure verification code which was sent to you by email Verification Code @@ -166,7 +175,7 @@ A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator. - Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time. + Please enter the password related to your account. Remember that passwords are sase sensitive. If you attempt to login to your account multiple times unsuccessfully, you will be locked out for a period of time. Password @@ -175,13 +184,13 @@ Password: - Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site + Specify if you would like to be signed back in automatically the next time you visit this site - Remember Me? + Stay Signed In? - Please Enter The Username Related To Your Account + Please enter the username related to your account Username @@ -201,7 +210,13 @@ Error Resetting Password - + + Error Sending Username Reminder + + + Error Sending Login Link + + Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions. @@ -228,6 +243,9 @@ The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider. + + Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process. + Register as new user? @@ -237,4 +255,13 @@ Passkey Login Was Not Successful + + Please enter the email address related to your account + + + Email Address + + + Email: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 6e7fb3e0..8d1bd113 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -567,4 +567,10 @@ Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)? + + Allow Login Link? + + + Do you want to allow users to login using a time sensitive link sent by email? + \ No newline at end of file diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 12685add..78d8f0af 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -101,7 +101,14 @@ namespace Oqtane.Services /// /// /// - Task ForgotPasswordAsync(User user); + Task ForgotPasswordAsync(User user); + + /// + /// Trigger a forgot-username e-mail for this . + /// + /// + /// + Task ForgotUsernameAsync(User user); /// /// Reset the password of this @@ -211,6 +218,13 @@ namespace Oqtane.Services /// /// Task DeleteLoginAsync(int userId, string provider, string key); + + /// + /// Send a login link + /// + /// + /// + Task SendLoginLinkAsync(User user); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -275,9 +289,14 @@ namespace Oqtane.Services return await PostJsonAsync($"{Apiurl}/verify?token={token}", user); } - public async Task ForgotPasswordAsync(User user) + public async Task ForgotPasswordAsync(User user) { - await PostJsonAsync($"{Apiurl}/forgot", user); + return await PostJsonAsync($"{Apiurl}/forgot", user); + } + + public async Task ForgotUsernameAsync(User user) + { + return await PostJsonAsync($"{Apiurl}/forgotusername", user); } public async Task ResetPasswordAsync(User user, string token) @@ -366,5 +385,10 @@ namespace Oqtane.Services { await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}"); } + + public async Task SendLoginLinkAsync(User user) + { + return await PostJsonAsync($"{Apiurl}/loginlink", user); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 018e2695..3d28e86e 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -296,12 +296,24 @@ namespace Oqtane.Controllers // POST api//forgot [HttpPost("forgot")] - public async Task Forgot([FromBody] User user) + public async Task Forgot([FromBody] User user) { if (ModelState.IsValid) { - await _userManager.ForgotPassword(user); + return await _userManager.ForgotPassword(user); } + return null; + } + + // POST api//forgotusername + [HttpPost("forgotusername")] + public async Task ForgotUsername([FromBody] User user) + { + if (ModelState.IsValid) + { + return await _userManager.ForgotUsername(user); + } + return null; } // POST api//reset @@ -559,5 +571,16 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } + + // POST api//loginlink + [HttpPost("loginlink")] + public async Task SendLoginLink([FromBody] User user) + { + if (ModelState.IsValid) + { + return await _userManager.SendLoginLink(user); + } + return null; + } } } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index e6f2661d..901c8ee8 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -4,10 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Security.Policy; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Localization; using Oqtane.Enums; @@ -30,7 +28,8 @@ namespace Oqtane.Managers Task LoginUser(User user, bool setCookie, bool isPersistent); Task LogoutUserEverywhere(User user); Task VerifyEmail(User user, string token); - Task ForgotPassword(User user); + Task ForgotPassword(User user); + Task ForgotUsername(User user); Task ResetPassword(User user, string token); User VerifyTwoFactor(User user, string token); Task ValidateUser(string username, string email, string password); @@ -42,6 +41,7 @@ namespace Oqtane.Managers Task> GetLogins(int userId, int siteId); Task AddLogin(User user, string token, string type, string key, string name); Task DeleteLogin(int userId, string provider, string key); + Task SendLoginLink(User user); } public class UserManager : IUserManager @@ -519,14 +519,16 @@ namespace Oqtane.Managers } return user; } - public async Task ForgotPassword(User user) + + public async Task ForgotPassword(User user) { IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); if (identityuser != null) { - var alias = _tenantManager.GetAlias(); - user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); + + var alias = _tenantManager.GetAlias(); + user = GetUser(user.Username, alias.SiteId); string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string siteName = _sites.GetSite(alias.SiteId).Name; string subject = _localizer["ForgotPasswordEmailSubject"]; @@ -537,11 +539,51 @@ namespace Oqtane.Managers body = body.Replace("[SiteName]", siteName); var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); _notifications.AddNotification(notification); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); + return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object } else { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username); + return null; + } + } + + public async Task ForgotUsername(User user) + { + try + { + IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email); + if (identityuser != null) + { + var alias = _tenantManager.GetAlias(); + user = GetUser(identityuser.UserName, alias.SiteId); + string url = alias.Protocol + alias.Name + "/login?name=" + user.Username; + string siteName = _sites.GetSite(alias.SiteId).Name; + string subject = _localizer["ForgotUsernameEmailSubject"]; + subject = subject.Replace("[SiteName]", siteName); + string body = _localizer["ForgotUsernameEmailBody"].Value; + body = body.Replace("[UserDisplayName]", user.DisplayName); + body = body.Replace("[URL]", url); + body = body.Replace("[SiteName]", siteName); + var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); + _notifications.AddNotification(notification); + + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email); + return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email); + return null; + } + } + catch + { + // email may not be unique + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email); + return null; } } @@ -588,6 +630,7 @@ namespace Oqtane.Managers } return user; } + public async Task ValidateUser(string username, string email, string password) { var validateResult = new UserValidateResult { Succeeded = true }; @@ -914,5 +957,49 @@ namespace Oqtane.Managers } } } + + public async Task SendLoginLink(User user) + { + try + { + IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email); + if (identityuser != null) + { + var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email"); + + var alias = _tenantManager.GetAlias(); + user = GetUser(identityuser.UserName, alias.SiteId); + user.TwoFactorCode = token; + user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); + _users.UpdateUser(user); + + string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string siteName = _sites.GetSite(alias.SiteId).Name; + string subject = _localizer["LoginLinkEmailSubject"]; + subject = subject.Replace("[SiteName]", siteName); + string body = _localizer["LoginLinkEmailBody"].Value; + body = body.Replace("[UserDisplayName]", user.DisplayName); + body = body.Replace("[URL]", url); + body = body.Replace("[SiteName]", siteName); + var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); + _notifications.AddNotification(notification); + + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email); + return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email); + return null; + } + } + catch + { + // email may not be unique + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email); + return null; + } + } + } } diff --git a/Oqtane.Server/Pages/LoginLink.cshtml b/Oqtane.Server/Pages/LoginLink.cshtml new file mode 100644 index 00000000..c069722c --- /dev/null +++ b/Oqtane.Server/Pages/LoginLink.cshtml @@ -0,0 +1,3 @@ +@page "/pages/loginlink" +@namespace Oqtane.Pages +@model Oqtane.Pages.LoginLinkModel diff --git a/Oqtane.Server/Pages/LoginLink.cshtml.cs b/Oqtane.Server/Pages/LoginLink.cshtml.cs new file mode 100644 index 00000000..01748f38 --- /dev/null +++ b/Oqtane.Server/Pages/LoginLink.cshtml.cs @@ -0,0 +1,69 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; +using Oqtane.Extensions; +using Oqtane.Infrastructure; +using Oqtane.Managers; +using Oqtane.Shared; + +namespace Oqtane.Pages +{ + [AllowAnonymous] + public class LoginLinkModel : PageModel + { + private readonly UserManager _identityUserManager; + private readonly SignInManager _identitySignInManager; + private readonly IUserManager _userManager; + private readonly ILogManager _logger; + + public LoginLinkModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) + { + _identityUserManager = identityUserManager; + _identitySignInManager = identitySignInManager; + _userManager = userManager; + _logger = logger; + } + + public async Task OnGetAsync(string name, string token) + { + var returnurl = "/login"; + + if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) && + !User.Identity.IsAuthenticated && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token)) + { + var validuser = false; + + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name); + if (identityuser != null) + { + var user = _userManager.GetUser(identityuser.UserName, HttpContext.GetAlias().SiteId); + if (user != null && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) + { + await _identitySignInManager.SignInAsync(identityuser, false); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name); + validuser = true; + returnurl = "/"; + } + } + + if (!validuser) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name); + returnurl += $"?status={ExternalLoginStatus.LoginLinkFailed}"; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name); + returnurl = "/"; + } + + return LocalRedirect(Url.Content("~" + returnurl)); + } + } +} diff --git a/Oqtane.Server/Resources/Managers/UserManager.resx b/Oqtane.Server/Resources/Managers/UserManager.resx index ecc38fb0..64c55528 100644 --- a/Oqtane.Server/Resources/Managers/UserManager.resx +++ b/Oqtane.Server/Resources/Managers/UserManager.resx @@ -121,7 +121,19 @@ Dear [UserDisplayName]<br><br>You recently requested to reset your password. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Reset Password</a></b><br><br>Please note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site.<br><br>If you did not request to reset your password you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team - Password Reset Notification Sent For [SiteName] + Password Reset Notification For [SiteName] + + + Dear [UserDisplayName]<br><br>You recently requested a username reminder. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Login</a></b><br><br>If you did not request a username reminder you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team + + + Forgotten Username Reminder For [SiteName] + + + Dear [UserDisplayName]<br><br>You recently requested a login link. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Login</a></b><br><br>Please note that the link is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate another login link request on the site.<br><br>If you did not request a login link you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team + + + Login Link Notification For [SiteName] Dear [UserDisplayName],<br><br>A user account has been successfully created for you with the username <b>[Username]</b>. Please <b><a href="[URL]">click here to login</a></b>. If you do not know your password, use the forgot password option on the login page to reset your account.<br><br>Thank You!<br>[SiteName] Team diff --git a/Oqtane.Shared/Shared/ExternalLoginStatus.cs b/Oqtane.Shared/Shared/ExternalLoginStatus.cs index 63cd0094..8423d799 100644 --- a/Oqtane.Shared/Shared/ExternalLoginStatus.cs +++ b/Oqtane.Shared/Shared/ExternalLoginStatus.cs @@ -10,5 +10,6 @@ namespace Oqtane.Shared { public const string AccessDenied = "AccessDenied"; public const string RemoteFailure = "RemoteFailure"; public const string ReviewClaims = "ReviewClaims"; + public const string LoginLinkFailed = "LoginLinkFailed"; } } From 6c6b36f3da1872cfcb53e4004e1c03ad865f9d9d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 14 Dec 2025 17:08:19 -0500 Subject: [PATCH 38/48] use a more complex token for login links --- Oqtane.Server/Managers/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 901c8ee8..1f08e348 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -965,7 +965,7 @@ namespace Oqtane.Managers IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email); if (identityuser != null) { - var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email"); + var token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); var alias = _tenantManager.GetAlias(); user = GetUser(identityuser.UserName, alias.SiteId); From b4f889671327fd0cb0d97367561ad31161516d08 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Mon, 15 Dec 2025 10:21:32 +0100 Subject: [PATCH 39/48] Add pagination state to Pager in Index.razor When clicking the Roles or Edit button, returning would load the first page. Pager now tracks and updates the current page using a new _page field and the CurrentPage/OnPageChange parameters. This improves pagination handling and user experience by persisting the current page state. --- Oqtane.Client/Modules/Admin/Users/Index.razor | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index cad17d8b..bad4ebe2 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -33,7 +33,7 @@ else

- +
    @@ -551,6 +551,7 @@ else @code { private List users; private string _deleted = "false"; + private int _page = 1; private string _allowregistration; private string _registerurl; From 7938eaf123760df9ac7321a59ce7458d60dd8ae0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 08:23:41 -0500 Subject: [PATCH 40/48] refactor new Forgot Username and Login Link methods --- Oqtane.Client/Modules/Admin/Login/Index.razor | 15 +- Oqtane.Client/Services/UserService.cs | 28 +-- Oqtane.Server/Controllers/UserController.cs | 36 ++-- Oqtane.Server/Managers/UserManager.cs | 162 +++++++++--------- 4 files changed, 114 insertions(+), 127 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 460aceab..4a6fee49 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -243,6 +243,9 @@ else private void SetAction(string action) { _action = action; + _username = ""; + _password = ""; + _email = ""; ClearModuleMessage(); StateHasChanged(); } @@ -364,9 +367,7 @@ else { if (!string.IsNullOrEmpty(_username)) { - var user = new User { Username = _username }; - user = await UserService.ForgotPasswordAsync(user); - if (user != null) + if (await UserService.ForgotPasswordAsync(_username)) { await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username); AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info); @@ -394,9 +395,7 @@ else { if (!string.IsNullOrEmpty(_email)) { - var user = new User { Email = _email }; - user = await UserService.ForgotUsernameAsync(user); - if (user != null) + if (await UserService.ForgotUsernameAsync(_email)) { AddModuleMessage(Localizer["Message.ForgotUsername"], MessageType.Info); await logger.LogInformation(LogFunction.Security, "Username Reminder Notification Sent For Email {Email}", _email); @@ -424,9 +423,7 @@ else { if (!string.IsNullOrEmpty(_email)) { - var user = new User { Email = _email }; - user = await UserService.SendLoginLinkAsync(user); - if (user != null) + if (await UserService.SendLoginLinkAsync(_email)) { AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info); await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email); diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 78d8f0af..437a23a6 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -97,18 +97,18 @@ namespace Oqtane.Services Task VerifyEmailAsync(User user, string token); /// - /// Trigger a forgot-password e-mail for this . + /// Trigger a forgot-password e-mail. /// - /// + /// /// - Task ForgotPasswordAsync(User user); + Task ForgotPasswordAsync(string username); /// - /// Trigger a forgot-username e-mail for this . + /// Trigger a username reminder e-mail. /// - /// + /// /// - Task ForgotUsernameAsync(User user); + Task ForgotUsernameAsync(string email); /// /// Reset the password of this @@ -222,9 +222,9 @@ namespace Oqtane.Services /// /// Send a login link /// - /// + /// /// - Task SendLoginLinkAsync(User user); + Task SendLoginLinkAsync(string email); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -289,14 +289,14 @@ namespace Oqtane.Services return await PostJsonAsync($"{Apiurl}/verify?token={token}", user); } - public async Task ForgotPasswordAsync(User user) + public async Task ForgotPasswordAsync(string username) { - return await PostJsonAsync($"{Apiurl}/forgot", user); + return await GetJsonAsync($"{Apiurl}/forgotpassword?name={username}"); } - public async Task ForgotUsernameAsync(User user) + public async Task ForgotUsernameAsync(string email) { - return await PostJsonAsync($"{Apiurl}/forgotusername", user); + return await GetJsonAsync($"{Apiurl}/forgotusername?email={email}"); } public async Task ResetPasswordAsync(User user, string token) @@ -386,9 +386,9 @@ namespace Oqtane.Services await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}"); } - public async Task SendLoginLinkAsync(User user) + public async Task SendLoginLinkAsync(string email) { - return await PostJsonAsync($"{Apiurl}/loginlink", user); + return await GetJsonAsync($"{Apiurl}/loginlink?email={email}"); } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 3d28e86e..57ea1641 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -294,26 +294,18 @@ namespace Oqtane.Controllers return user; } - // POST api//forgot - [HttpPost("forgot")] - public async Task Forgot([FromBody] User user) + // GET api//forgotpassword?name=x + [HttpGet("forgotpassword")] + public async Task ForgotPassword(string name) { - if (ModelState.IsValid) - { - return await _userManager.ForgotPassword(user); - } - return null; + return await _userManager.ForgotPassword(name); } - // POST api//forgotusername - [HttpPost("forgotusername")] - public async Task ForgotUsername([FromBody] User user) + // GET api//forgotusername?email=x + [HttpGet("forgotusername")] + public async Task ForgotUsername(string email) { - if (ModelState.IsValid) - { - return await _userManager.ForgotUsername(user); - } - return null; + return await _userManager.ForgotUsername(email); } // POST api//reset @@ -572,15 +564,11 @@ namespace Oqtane.Controllers } } - // POST api//loginlink - [HttpPost("loginlink")] - public async Task SendLoginLink([FromBody] User user) + // GET api//loginlink?email=x + [HttpGet("loginlink")] + public async Task SendLoginLink(string email) { - if (ModelState.IsValid) - { - return await _userManager.SendLoginLink(user); - } - return null; + return await _userManager.SendLoginLink(email); } } } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 1f08e348..a9c2d5e0 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -28,8 +28,8 @@ namespace Oqtane.Managers Task LoginUser(User user, bool setCookie, bool isPersistent); Task LogoutUserEverywhere(User user); Task VerifyEmail(User user, string token); - Task ForgotPassword(User user); - Task ForgotUsername(User user); + Task ForgotPassword(string username); + Task ForgotUsername(string email); Task ResetPassword(User user, string token); User VerifyTwoFactor(User user, string token); Task ValidateUser(string username, string email, string password); @@ -41,7 +41,7 @@ namespace Oqtane.Managers Task> GetLogins(int userId, int siteId); Task AddLogin(User user, string token, string type, string key, string name); Task DeleteLogin(int userId, string provider, string key); - Task SendLoginLink(User user); + Task SendLoginLink(string email); } public class UserManager : IUserManager @@ -520,70 +520,72 @@ namespace Oqtane.Managers return user; } - public async Task ForgotPassword(User user) + public async Task ForgotPassword(string username) { - IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); - if (identityuser != null) + if (!string.IsNullOrEmpty(username)) { - string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); - - var alias = _tenantManager.GetAlias(); - user = GetUser(user.Username, alias.SiteId); - string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string siteName = _sites.GetSite(alias.SiteId).Name; - string subject = _localizer["ForgotPasswordEmailSubject"]; - subject = subject.Replace("[SiteName]", siteName); - string body = _localizer["ForgotPasswordEmailBody"].Value; - body = body.Replace("[UserDisplayName]", user.DisplayName); - body = body.Replace("[URL]", url); - body = body.Replace("[SiteName]", siteName); - var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); - _notifications.AddNotification(notification); - - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); - return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username); - return null; - } - } - - public async Task ForgotUsername(User user) - { - try - { - IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email); + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username); if (identityuser != null) { + string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); + var alias = _tenantManager.GetAlias(); - user = GetUser(identityuser.UserName, alias.SiteId); - string url = alias.Protocol + alias.Name + "/login?name=" + user.Username; + var user = GetUser(username, alias.SiteId); + string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string siteName = _sites.GetSite(alias.SiteId).Name; - string subject = _localizer["ForgotUsernameEmailSubject"]; + string subject = _localizer["ForgotPasswordEmailSubject"]; subject = subject.Replace("[SiteName]", siteName); - string body = _localizer["ForgotUsernameEmailBody"].Value; + string body = _localizer["ForgotPasswordEmailBody"].Value; body = body.Replace("[UserDisplayName]", user.DisplayName); body = body.Replace("[URL]", url); body = body.Replace("[SiteName]", siteName); var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); _notifications.AddNotification(notification); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email); - return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email); - return null; + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); + return true; } } - catch + + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", username); + return false; + } + + public async Task ForgotUsername(string email) + { + try + { + if (!string.IsNullOrEmpty(email)) + { + IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(email); + if (identityuser != null) + { + var alias = _tenantManager.GetAlias(); + var user = GetUser(identityuser.UserName, alias.SiteId); + string url = alias.Protocol + alias.Name + "/login?name=" + user.Username; + string siteName = _sites.GetSite(alias.SiteId).Name; + string subject = _localizer["ForgotUsernameEmailSubject"]; + subject = subject.Replace("[SiteName]", siteName); + string body = _localizer["ForgotUsernameEmailBody"].Value; + body = body.Replace("[UserDisplayName]", user.DisplayName); + body = body.Replace("[URL]", url); + body = body.Replace("[SiteName]", siteName); + var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); + _notifications.AddNotification(notification); + + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email); + return true; + } + } + + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", email); + return false; + } + catch (Exception ex) { // email may not be unique - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email); - return null; + _logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Forgot Username Notification Failed For {Email}", email); + return false; } } @@ -958,48 +960,48 @@ namespace Oqtane.Managers } } - public async Task SendLoginLink(User user) + public async Task SendLoginLink(string email) { try { - IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email); - if (identityuser != null) + if (!string.IsNullOrEmpty(email)) { - var token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(email); + if (identityuser != null) + { + var token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - var alias = _tenantManager.GetAlias(); - user = GetUser(identityuser.UserName, alias.SiteId); - user.TwoFactorCode = token; - user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); - _users.UpdateUser(user); + var alias = _tenantManager.GetAlias(); + var user = GetUser(identityuser.UserName, alias.SiteId); + user.TwoFactorCode = token; + user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); + _users.UpdateUser(user); - string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string siteName = _sites.GetSite(alias.SiteId).Name; - string subject = _localizer["LoginLinkEmailSubject"]; - subject = subject.Replace("[SiteName]", siteName); - string body = _localizer["LoginLinkEmailBody"].Value; - body = body.Replace("[UserDisplayName]", user.DisplayName); - body = body.Replace("[URL]", url); - body = body.Replace("[SiteName]", siteName); - var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); - _notifications.AddNotification(notification); + string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string siteName = _sites.GetSite(alias.SiteId).Name; + string subject = _localizer["LoginLinkEmailSubject"]; + subject = subject.Replace("[SiteName]", siteName); + string body = _localizer["LoginLinkEmailBody"].Value; + body = body.Replace("[UserDisplayName]", user.DisplayName); + body = body.Replace("[URL]", url); + body = body.Replace("[SiteName]", siteName); + var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body); + _notifications.AddNotification(notification); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email); - return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email); - return null; + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email); + return true; // minimal object + } } + + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", email); + return false; } - catch + catch (Exception ex) { // email may not be unique - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email); - return null; + _logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Login Link Notification Failed For {Email}", email); + return false; } } - } } From 19587871859c0a65c88dca233366898bdfc6c8c0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 09:02:25 -0500 Subject: [PATCH 41/48] relocate the GetUser() call in App.razor so that it is not included in the Site cache --- Oqtane.Server/Services/SiteService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 14b28c2a..ef7b32d6 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -104,6 +104,12 @@ namespace Oqtane.Services } site.Languages = site.Languages.OrderBy(item => item.Name).ToList(); + // get user + if (_accessor.HttpContext.User.IsAuthenticated()) + { + site.User = _userManager.GetUser(_accessor.HttpContext.User.UserId(), site.SiteId); + } + return Task.FromResult(site); } @@ -149,12 +155,6 @@ namespace Oqtane.Services // themes site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList()); - // user - if (_accessor.HttpContext.User.IsAuthenticated()) - { - site.User = _userManager.GetUser(_accessor.HttpContext.User.UserId(), site.SiteId); - } - // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); From a48dff4a85179e3a380f9cf1025709b10ff783d9 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 10:29:03 -0500 Subject: [PATCH 42/48] improve new API method signatures --- Oqtane.Client/Services/UserService.cs | 6 +++--- Oqtane.Server/Controllers/UserController.cs | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 437a23a6..76de84e0 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -291,12 +291,12 @@ namespace Oqtane.Services public async Task ForgotPasswordAsync(string username) { - return await GetJsonAsync($"{Apiurl}/forgotpassword?name={username}"); + return await GetJsonAsync($"{Apiurl}/forgotpassword/{WebUtility.UrlEncode(username)}"); } public async Task ForgotUsernameAsync(string email) { - return await GetJsonAsync($"{Apiurl}/forgotusername?email={email}"); + return await GetJsonAsync($"{Apiurl}/forgotusername/{WebUtility.UrlEncode(email)}"); } public async Task ResetPasswordAsync(User user, string token) @@ -388,7 +388,7 @@ namespace Oqtane.Services public async Task SendLoginLinkAsync(string email) { - return await GetJsonAsync($"{Apiurl}/loginlink?email={email}"); + return await GetJsonAsync($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}"); } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 57ea1641..ece7d295 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; -using System.Security.Policy; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; @@ -294,15 +293,15 @@ namespace Oqtane.Controllers return user; } - // GET api//forgotpassword?name=x - [HttpGet("forgotpassword")] - public async Task ForgotPassword(string name) + // GET api//forgotpassword/x + [HttpGet("forgotpassword/{username}")] + public async Task ForgotPassword(string username) { - return await _userManager.ForgotPassword(name); + return await _userManager.ForgotPassword(username); } - // GET api//forgotusername?email=x - [HttpGet("forgotusername")] + // GET api//forgotusername/x + [HttpGet("forgotusername/{email}")] public async Task ForgotUsername(string email) { return await _userManager.ForgotUsername(email); @@ -564,8 +563,8 @@ namespace Oqtane.Controllers } } - // GET api//loginlink?email=x - [HttpGet("loginlink")] + // GET api//loginlink/x + [HttpGet("loginlink/{email}")] public async Task SendLoginLink(string email) { return await _userManager.SendLoginLink(email); From 87fd9dd00066535189cc4beef5861932fc37e185 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 10:43:11 -0500 Subject: [PATCH 43/48] use EmailConfirmationToken (which is valid for 10 minutes) --- Oqtane.Server/Managers/UserManager.cs | 4 ---- Oqtane.Server/Pages/LoginLink.cshtml.cs | 8 +++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index a9c2d5e0..5a1a9d01 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -973,10 +973,6 @@ namespace Oqtane.Managers var alias = _tenantManager.GetAlias(); var user = GetUser(identityuser.UserName, alias.SiteId); - user.TwoFactorCode = token; - user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); - _users.UpdateUser(user); - string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string siteName = _sites.GetSite(alias.SiteId).Name; string subject = _localizer["LoginLinkEmailSubject"]; diff --git a/Oqtane.Server/Pages/LoginLink.cshtml.cs b/Oqtane.Server/Pages/LoginLink.cshtml.cs index 01748f38..d090c7ed 100644 --- a/Oqtane.Server/Pages/LoginLink.cshtml.cs +++ b/Oqtane.Server/Pages/LoginLink.cshtml.cs @@ -18,14 +18,12 @@ namespace Oqtane.Pages { private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; - private readonly IUserManager _userManager; private readonly ILogManager _logger; - public LoginLinkModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) + public LoginLinkModel(UserManager identityUserManager, SignInManager identitySignInManager, ILogManager logger) { _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; - _userManager = userManager; _logger = logger; } @@ -41,8 +39,8 @@ namespace Oqtane.Pages IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name); if (identityuser != null) { - var user = _userManager.GetUser(identityuser.UserName, HttpContext.GetAlias().SiteId); - if (user != null && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) + var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token); + if (result.Succeeded) { await _identitySignInManager.SignInAsync(identityuser, false); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name); From c539f41ebf509a3671497ce27a8225a2d2803ae4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 10:47:01 -0500 Subject: [PATCH 44/48] remove unnecessary comment --- Oqtane.Server/Managers/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 5a1a9d01..3e032400 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -985,7 +985,7 @@ namespace Oqtane.Managers _notifications.AddNotification(notification); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email); - return true; // minimal object + return true; } } From f5f00c51c1d50d90809a022553ddab0d3aa578df Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 11:16:37 -0500 Subject: [PATCH 45/48] limit management of user settings to host users --- Oqtane.Client/Modules/Admin/Users/Index.razor | 862 +++++++++--------- .../Resources/Modules/Admin/Users/Index.resx | 4 +- 2 files changed, 430 insertions(+), 436 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index bad4ebe2..4a796298 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -60,9 +60,46 @@ else - +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
@@ -72,475 +109,432 @@ else
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + @if (_allowregistration == "true") { - @if (_allowregistration == "true") - { -
- -
- -
-
- }
- +
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- +
} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { -
-
- -
- -
-
-
- -
- -
-
-
- -
- +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+ + @if (!string.IsNullOrEmpty(_providerurl)) + { + @Localizer["Info"] + }
-
+ +
+
+
+ +
+ +
+
+ @if (_providertype != "") + {
- +
- +
-
+
+ } + @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) + {
- +
- +
- +
- +
-
- -
+
+ } + @if (_providertype == AuthenticationProviderTypes.OAuth2) + {
- +
- +
-
+
- +
- +
-
- -
+
- + +
+ +
+
+ } + @if (_providertype != "") + { +
+ +
+ +
+
+
+
- - @if (!string.IsNullOrEmpty(_providerurl)) - { - @Localizer["Info"] - } + +
-
-
- -
- -
-
- @if (_providertype != "") - { -
- -
- -
-
- } @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) {
- +
- +
- +
- + +
+
+
+ +
+
} - @if (_providertype == AuthenticationProviderTypes.OAuth2) - { -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- } - @if (_providertype != "") - { -
- -
- -
-
-
- -
-
- - -
-
-
- @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) - { -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- } -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
- - @if (_reviewclaims == "true") - { - @SharedLocalizer["Test"] - } -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { -
- -
- -
-
-
- -
- -
-
- } - } - -
- + +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
- - + + @if (_reviewclaims == "true") + { + @SharedLocalizer["Test"] + }
-
+
- +
- +
-
+
- +
- +
-
+
- +
- +
-
+
- + +
+ +
+
+
+ +
+ +
+
+
+
- - +
-
- - } + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+ +
+ +
+
+ } + } + +
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+

@@ -553,13 +547,14 @@ else private string _deleted = "false"; private int _page = 1; - private string _allowregistration; - private string _registerurl; - private string _profileurl; - private string _requireconfirmedemail; + private string _allowsitelogin; + private string _twofactor; private string _loginlink; private string _passkeys; - private string _twofactor; + private string _allowregistration; + private string _registerurl; + private string _requireconfirmedemail; + private string _profileurl; private string _cookiename; private string _cookiedomain; private string _cookieexpiration; @@ -609,7 +604,6 @@ else private string _createusers; private string _verifyusers; private string _allowhostrole; - private string _allowsitelogin; private string _secret; private string _secrettype = "password"; @@ -633,12 +627,13 @@ else if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); - _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); - _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true"); + _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); + _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); _loginlink = SettingService.GetSetting(settings, "LoginOptions:LoginLink", "false"); _passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false"); - _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); + _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); + _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true"); + _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application"); _cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", ""); _cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", ""); @@ -700,7 +695,6 @@ else _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); _allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false"); - _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); } private async Task LoadUsersAsync() @@ -765,20 +759,21 @@ else { try { - var site = PageState.Site; - site.AllowRegistration = bool.Parse(_allowregistration); - await SiteService.UpdateSiteAsync(site); - - var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); - settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); - settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false); + var site = PageState.Site; + site.AllowRegistration = bool.Parse(_allowregistration); + await SiteService.UpdateSiteAsync(site); + + var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); + + settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); + settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:LoginLink", _loginlink, false); settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false); - settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); + settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); + settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false); + settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); @@ -824,16 +819,15 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true); - settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true); - } - await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); - await SettingService.ClearSiteSettingsCacheAsync(); + await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); + await SettingService.ClearSiteSettingsCacheAsync(); + } if (!string.IsNullOrEmpty(_secret)) { diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 8d1bd113..57eecaca 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -217,7 +217,7 @@ Unique Characters: - Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site. + Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already successfully configured an alternate login method, or else you may lock yourself out of the site. Allow Local Login? @@ -370,7 +370,7 @@ Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out. - Two Factor Authentication? + Use 2FA? Do you want to require registered users to verify their email address before they are allowed to log in? From 0d5bb3f3b318dad57d83761e90eda199707e280a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 11:28:51 -0500 Subject: [PATCH 46/48] increase width of login component --- .../wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css index 38c1a2c9..38010da6 100644 --- a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css +++ b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css @@ -1,5 +1,5 @@ /* Login Module Custom Styles */ .Oqtane-Modules-Admin-Login { - width: 220px; + width: 280px; } From 0f791253bafb7406dc803dfe915596e32e90bb44 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 15 Dec 2025 13:59:30 -0500 Subject: [PATCH 47/48] update azuredeploy.json --- azuredeploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredeploy.json b/azuredeploy.json index 4b3df91b..54ab2451 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/v10.0.0/Oqtane.Framework.10.0.0.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.1/Oqtane.Framework.10.0.1.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" From 68a6e6862ec64a7c38b736270d39e9aaca8c631a Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 15 Dec 2025 14:02:25 -0500 Subject: [PATCH 48/48] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f15b4995..03b1c488 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 -[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) was released on November 14, 2025 and is a major release including 77 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) was released on December 15, 2025 and is a major release including 38 pull requests by 5 different contributors, pushing the total number of project commits all-time over 7400. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -111,6 +111,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... +[10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) (Dec 15, 2025) +- [x] Stabilization improvements + [10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) (Nov 14, 2025) - [x] Migration to .NET 10 - [x] Passkey Authentication