From 628129c08d124fe7917a10c5f8cf25a5070a21ac Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 11 Feb 2025 12:00:41 -0500 Subject: [PATCH 01/60] Update README.md --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index de03e405..468612c0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) was released on December 20, 2024 and is a maintenance release including 58 pull requests by 7 different contributors, pushing the total number of project commits all-time to over 6100. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0) was released on February 11, 2025 and is a minor release including 95 pull requests by 9 different contributors, pushing the total number of project commits all-time to over 6300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) @@ -20,7 +20,7 @@ Oqtane is being developed based on some fundamental principles which are outline **Installing using source code from the Dev/Master branch:** -- Install **[.NET 9.0.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.1 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. @@ -85,10 +85,14 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... - + +[6.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0) (Feb 11, 2025) +- [x] Static Asset / Folder Asset Caching +- [x] JavaScript improvements in Blazor Static Server Rendering (SSR) +- [x] User Impersonation + [6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) (Dec 20, 2024) - [x] Stabilization improvements -- [x] JavaScript improvements in Blazor Static Server Rendering (SSR) [6.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0) (Nov 14, 2024) - [x] Migration to .NET 9 From aff99acfaeb04f43bb8118eefdd030768ffc802a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 12 Feb 2025 20:18:10 +0800 Subject: [PATCH 02/60] Fix #5054: resolve the issue in fa-IR language. --- .../ApplicationBuilderExtensions.cs | 9 +++ .../Localization/PersianCalendar.cs | 77 +++++++++++++++++++ .../Localization/PersianCulture.cs | 70 +++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs create mode 100644 Oqtane.Server/Infrastructure/Localization/PersianCulture.cs diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index 3e466a20..87bf5305 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -34,6 +34,15 @@ namespace Oqtane.Extensions options.SetDefaultCulture(defaultCulture) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); + + for (var i = 0; i < options.SupportedCultures.Count; i++) + { + if (options.SupportedCultures[i].Name.Equals("fa-IR", StringComparison.OrdinalIgnoreCase)) + { + options.SupportedCultures[i] = PersianCulture.GetPersianCultureInfo(); + break; + } + } }); return app; diff --git a/Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs b/Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs new file mode 100644 index 00000000..1b900031 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs @@ -0,0 +1,77 @@ +using System; + +namespace Oqtane.Infrastructure +{ + public class PersianCalendar : System.Globalization.PersianCalendar + { + public override int GetYear(DateTime time) + { + try + { + return base.GetYear(time); + } + catch + { + // ignore + } + + return time.Year; + } + + public override int GetMonth(DateTime time) + { + try + { + return base.GetMonth(time); + } + catch + { + // ignore + } + + return time.Month; + } + + public override int GetDayOfMonth(DateTime time) + { + try + { + return base.GetDayOfMonth(time); + } + catch + { + // ignore + } + + return time.Day; + } + + public override int GetDayOfYear(DateTime time) + { + try + { + return base.GetDayOfYear(time); + } + catch + { + // ignore + } + + return time.DayOfYear; + } + + public override DayOfWeek GetDayOfWeek(DateTime time) + { + try + { + return base.GetDayOfWeek(time); + } + catch + { + // ignore + } + + return time.DayOfWeek; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs b/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs new file mode 100644 index 00000000..addd73a2 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs @@ -0,0 +1,70 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace Oqtane.Infrastructure +{ + public class PersianCulture + { + public static CultureInfo GetPersianCultureInfo() + { + var persianCultureInfo = new CultureInfo("fa-IR"); + + SetPersianDateTimeFormatInfo(persianCultureInfo.DateTimeFormat); + SetNumberFormatInfo(persianCultureInfo.NumberFormat); + + var cal = new PersianCalendar(); + + FieldInfo fieldInfo = persianCultureInfo.GetType().GetField("_calendar", BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo != null) + { + fieldInfo.SetValue(persianCultureInfo, cal); + } + + FieldInfo info = persianCultureInfo.DateTimeFormat.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance); + if (info != null) + { + info.SetValue(persianCultureInfo.DateTimeFormat, cal); + } + + return persianCultureInfo; + } + + public static void SetPersianDateTimeFormatInfo(DateTimeFormatInfo persianDateTimeFormatInfo) + { + persianDateTimeFormatInfo.MonthNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty }; + persianDateTimeFormatInfo.MonthGenitiveNames = persianDateTimeFormatInfo.MonthNames; + persianDateTimeFormatInfo.AbbreviatedMonthNames = persianDateTimeFormatInfo.MonthNames; + persianDateTimeFormatInfo.AbbreviatedMonthGenitiveNames = persianDateTimeFormatInfo.MonthNames; + + persianDateTimeFormatInfo.DayNames = new[] { "یکشنبه", "دوشنبه", "ﺳﻪشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه" }; + persianDateTimeFormatInfo.AbbreviatedDayNames = new[] { "ی", "د", "س", "چ", "پ", "ج", "ش" }; + persianDateTimeFormatInfo.ShortestDayNames = persianDateTimeFormatInfo.AbbreviatedDayNames; + persianDateTimeFormatInfo.FirstDayOfWeek = DayOfWeek.Saturday; + + persianDateTimeFormatInfo.AMDesignator = "ق.ظ"; + persianDateTimeFormatInfo.PMDesignator = "ب.ظ"; + + persianDateTimeFormatInfo.DateSeparator = "/"; + persianDateTimeFormatInfo.TimeSeparator = ":"; + + persianDateTimeFormatInfo.FullDateTimePattern = "tt hh:mm:ss yyyy mmmm dd dddd"; + persianDateTimeFormatInfo.YearMonthPattern = "yyyy, MMMM"; + persianDateTimeFormatInfo.MonthDayPattern = "dd MMMM"; + + persianDateTimeFormatInfo.LongDatePattern = "dddd, dd MMMM,yyyy"; + persianDateTimeFormatInfo.ShortDatePattern = "yyyy/MM/dd"; + + persianDateTimeFormatInfo.LongTimePattern = "hh:mm:ss tt"; + persianDateTimeFormatInfo.ShortTimePattern = "hh:mm tt"; + } + + public static void SetNumberFormatInfo(NumberFormatInfo persianNumberFormatInfo) + { + persianNumberFormatInfo.NumberDecimalSeparator = "/"; + persianNumberFormatInfo.DigitSubstitution = DigitShapes.NativeNational; + persianNumberFormatInfo.NumberNegativePattern = 0; + persianNumberFormatInfo.NegativeSign = "-"; + } + } +} From c40a483ffa2da237bf50da8be8064584e67367d1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 14 Feb 2025 09:09:00 -0500 Subject: [PATCH 03/60] add another constructor for Script class --- Oqtane.Shared/Models/Script.cs | 9 +++++++++ Oqtane.Shared/Shared/Constants.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Oqtane.Shared/Models/Script.cs b/Oqtane.Shared/Models/Script.cs index 1bcdceb6..6654fbe1 100644 --- a/Oqtane.Shared/Models/Script.cs +++ b/Oqtane.Shared/Models/Script.cs @@ -32,6 +32,15 @@ namespace Oqtane.Models this.CrossOrigin = CrossOrigin; } + public Script(string Src, string Integrity, string CrossOrigin, ResourceLoadBehavior LoadBehavior) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + this.LoadBehavior = LoadBehavior; + } + public Script(string Src, string Integrity, string CrossOrigin, ResourceLocation Location, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string Type, string Bundle, string RenderMode) { SetDefaults(); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 447da2fa..024fc1a7 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -46,7 +46,7 @@ namespace Oqtane.Shared public const string DefaultSite = "Default Site"; public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp"; - public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,xml,rss,css"; + public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,rss,css"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$"; public static readonly char[] InvalidFileNameChars = From 049ddef53162409594858090ad0627c3c3888907 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 14 Feb 2025 09:17:44 -0500 Subject: [PATCH 04/60] synchronize BlazorScriptReload changes --- Oqtane.Server/wwwroot/js/reload.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 6766e74d..1b058f4e 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -1,8 +1,8 @@ const scriptKeys = new Set(); export function onUpdate() { - // determine if this is an enhanced navigation - let enhancedNavigation = scriptKeys.size !== 0; + // determine if this is an initial request + let initialRequest = scriptKeys.size === 0; // iterate over all script elements in document const scripts = document.getElementsByTagName('script'); @@ -11,7 +11,7 @@ export function onUpdate() { if (script.hasAttribute('data-reload')) { let key = getKey(script); - if (enhancedNavigation) { + if (!initialRequest) { // reload the script if data-reload is "always" or "true"... or if the script has not been loaded previously and data-reload is "once" let dataReload = script.getAttribute('data-reload'); if ((dataReload === 'always' || dataReload === 'true') || (!scriptKeys.has(key) && dataReload == 'once')) { @@ -40,7 +40,7 @@ function getKey(script) { function reloadScript(script) { try { if (isValid(script)) { - replaceScript(script); + injectScript(script); } } catch (error) { console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error); @@ -55,16 +55,17 @@ function isValid(script) { return true; } -function replaceScript(script) { +function injectScript(script) { return new Promise((resolve, reject) => { var newScript = document.createElement('script'); // replicate attributes and content for (let i = 0; i < script.attributes.length; i++) { - newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); + if (script.attributes[i].name !== 'data-reload') { + newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); + } } newScript.innerHTML = script.innerHTML; - newScript.removeAttribute('data-reload'); // dynamically injected scripts cannot be async or deferred newScript.async = false; @@ -73,10 +74,10 @@ function replaceScript(script) { newScript.onload = () => resolve(); newScript.onerror = (error) => reject(error); - // remove existing script element - script.remove(); - - // replace with new script element to force reload in Blazor + // inject script element in head to force execution in Blazor document.head.appendChild(newScript); + + // remove data-reload attribute + script.removeAttribute('data-reload'); }); } \ No newline at end of file From 94b03d2a6b97b02371e0ee32e819bfeae8626d49 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 16 Feb 2025 10:25:43 +0800 Subject: [PATCH 05/60] update settings for all RTL languages. --- .../ApplicationBuilderExtensions.cs | 7 +- .../Localization/PersianCulture.cs | 70 ------------------- .../Localization/RightToLeftCulture.cs | 42 +++++++++++ ...endar.cs => RightToLeftCultureCalendar.cs} | 2 +- 4 files changed, 46 insertions(+), 75 deletions(-) delete mode 100644 Oqtane.Server/Infrastructure/Localization/PersianCulture.cs create mode 100644 Oqtane.Server/Infrastructure/Localization/RightToLeftCulture.cs rename Oqtane.Server/Infrastructure/Localization/{PersianCalendar.cs => RightToLeftCultureCalendar.cs} (94%) diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index 87bf5305..4225f0b8 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -35,12 +35,11 @@ namespace Oqtane.Extensions .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); - for (var i = 0; i < options.SupportedCultures.Count; i++) + foreach(var culture in options.SupportedCultures) { - if (options.SupportedCultures[i].Name.Equals("fa-IR", StringComparison.OrdinalIgnoreCase)) + if (culture.TextInfo.IsRightToLeft) { - options.SupportedCultures[i] = PersianCulture.GetPersianCultureInfo(); - break; + RightToLeftCulture.ResolveFormat(culture); } } }); diff --git a/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs b/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs deleted file mode 100644 index addd73a2..00000000 --- a/Oqtane.Server/Infrastructure/Localization/PersianCulture.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Globalization; -using System.Reflection; - -namespace Oqtane.Infrastructure -{ - public class PersianCulture - { - public static CultureInfo GetPersianCultureInfo() - { - var persianCultureInfo = new CultureInfo("fa-IR"); - - SetPersianDateTimeFormatInfo(persianCultureInfo.DateTimeFormat); - SetNumberFormatInfo(persianCultureInfo.NumberFormat); - - var cal = new PersianCalendar(); - - FieldInfo fieldInfo = persianCultureInfo.GetType().GetField("_calendar", BindingFlags.NonPublic | BindingFlags.Instance); - if (fieldInfo != null) - { - fieldInfo.SetValue(persianCultureInfo, cal); - } - - FieldInfo info = persianCultureInfo.DateTimeFormat.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance); - if (info != null) - { - info.SetValue(persianCultureInfo.DateTimeFormat, cal); - } - - return persianCultureInfo; - } - - public static void SetPersianDateTimeFormatInfo(DateTimeFormatInfo persianDateTimeFormatInfo) - { - persianDateTimeFormatInfo.MonthNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty }; - persianDateTimeFormatInfo.MonthGenitiveNames = persianDateTimeFormatInfo.MonthNames; - persianDateTimeFormatInfo.AbbreviatedMonthNames = persianDateTimeFormatInfo.MonthNames; - persianDateTimeFormatInfo.AbbreviatedMonthGenitiveNames = persianDateTimeFormatInfo.MonthNames; - - persianDateTimeFormatInfo.DayNames = new[] { "یکشنبه", "دوشنبه", "ﺳﻪشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه" }; - persianDateTimeFormatInfo.AbbreviatedDayNames = new[] { "ی", "د", "س", "چ", "پ", "ج", "ش" }; - persianDateTimeFormatInfo.ShortestDayNames = persianDateTimeFormatInfo.AbbreviatedDayNames; - persianDateTimeFormatInfo.FirstDayOfWeek = DayOfWeek.Saturday; - - persianDateTimeFormatInfo.AMDesignator = "ق.ظ"; - persianDateTimeFormatInfo.PMDesignator = "ب.ظ"; - - persianDateTimeFormatInfo.DateSeparator = "/"; - persianDateTimeFormatInfo.TimeSeparator = ":"; - - persianDateTimeFormatInfo.FullDateTimePattern = "tt hh:mm:ss yyyy mmmm dd dddd"; - persianDateTimeFormatInfo.YearMonthPattern = "yyyy, MMMM"; - persianDateTimeFormatInfo.MonthDayPattern = "dd MMMM"; - - persianDateTimeFormatInfo.LongDatePattern = "dddd, dd MMMM,yyyy"; - persianDateTimeFormatInfo.ShortDatePattern = "yyyy/MM/dd"; - - persianDateTimeFormatInfo.LongTimePattern = "hh:mm:ss tt"; - persianDateTimeFormatInfo.ShortTimePattern = "hh:mm tt"; - } - - public static void SetNumberFormatInfo(NumberFormatInfo persianNumberFormatInfo) - { - persianNumberFormatInfo.NumberDecimalSeparator = "/"; - persianNumberFormatInfo.DigitSubstitution = DigitShapes.NativeNational; - persianNumberFormatInfo.NumberNegativePattern = 0; - persianNumberFormatInfo.NegativeSign = "-"; - } - } -} diff --git a/Oqtane.Server/Infrastructure/Localization/RightToLeftCulture.cs b/Oqtane.Server/Infrastructure/Localization/RightToLeftCulture.cs new file mode 100644 index 00000000..8d07220b --- /dev/null +++ b/Oqtane.Server/Infrastructure/Localization/RightToLeftCulture.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace Oqtane.Infrastructure +{ + public class RightToLeftCulture + { + public static CultureInfo ResolveFormat(CultureInfo cultureInfo) + { + SetNumberFormatInfo(cultureInfo.NumberFormat); + SetCalenar(cultureInfo); + + return cultureInfo; + } + + private static void SetCalenar(CultureInfo cultureInfo) + { + var calendar = new RightToLeftCultureCalendar(); + + var fieldInfo = cultureInfo.GetType().GetField("_calendar", BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo != null) + { + fieldInfo.SetValue(cultureInfo, calendar); + } + + var info = cultureInfo.DateTimeFormat.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance); + if (info != null) + { + info.SetValue(cultureInfo.DateTimeFormat, calendar); + } + } + + public static void SetNumberFormatInfo(NumberFormatInfo persianNumberFormatInfo) + { + persianNumberFormatInfo.NumberDecimalSeparator = "."; + persianNumberFormatInfo.DigitSubstitution = DigitShapes.NativeNational; + persianNumberFormatInfo.NumberNegativePattern = 0; + persianNumberFormatInfo.NegativeSign = "-"; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs b/Oqtane.Server/Infrastructure/Localization/RightToLeftCultureCalendar.cs similarity index 94% rename from Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs rename to Oqtane.Server/Infrastructure/Localization/RightToLeftCultureCalendar.cs index 1b900031..56b420d9 100644 --- a/Oqtane.Server/Infrastructure/Localization/PersianCalendar.cs +++ b/Oqtane.Server/Infrastructure/Localization/RightToLeftCultureCalendar.cs @@ -2,7 +2,7 @@ using System; namespace Oqtane.Infrastructure { - public class PersianCalendar : System.Globalization.PersianCalendar + public class RightToLeftCultureCalendar : System.Globalization.PersianCalendar { public override int GetYear(DateTime time) { From 2e6ab398d93b76c048803aed0a93c8243f549c8d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 17 Feb 2025 13:21:58 -0500 Subject: [PATCH 06/60] fix visitor purge logic --- Oqtane.Server/Repository/VisitorRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Repository/VisitorRepository.cs b/Oqtane.Server/Repository/VisitorRepository.cs index f6b349ca..d3583079 100644 --- a/Oqtane.Server/Repository/VisitorRepository.cs +++ b/Oqtane.Server/Repository/VisitorRepository.cs @@ -65,14 +65,14 @@ namespace Oqtane.Repository // delete visitors in batches of 100 records var count = 0; var purgedate = DateTime.UtcNow.AddDays(-age); - var visitors = db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate) + var visitors = db.Visitor.Where(item => item.SiteId == siteId && item.VisitedOn < purgedate) .OrderBy(item => item.VisitedOn).Take(100).ToList(); while (visitors.Count > 0) { count += visitors.Count; db.Visitor.RemoveRange(visitors); db.SaveChanges(); - visitors = db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate) + visitors = db.Visitor.Where(item => item.SiteId == siteId && item.VisitedOn < purgedate) .OrderBy(item => item.VisitedOn).Take(100).ToList(); } return count; From 2df05b4afd61ed00b8bf1d0b9bb750136d7c5e02 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 18 Feb 2025 08:53:23 -0500 Subject: [PATCH 07/60] make purge job output more readable --- Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 07504f1e..5cf4c59c 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -39,7 +39,7 @@ namespace Oqtane.Infrastructure List sites = siteRepository.GetSites().ToList(); foreach (Site site in sites) { - log += "Processing Site: " + site.Name + "
"; + log += "
Processing Site: " + site.Name + "
"; int retention; int count; @@ -118,11 +118,11 @@ namespace Oqtane.Infrastructure try { var assemblies = installationManager.RegisterAssemblies(); - log += assemblies.ToString() + " Assemblies Registered
"; + log += "
" + assemblies.ToString() + " Assemblies Registered
"; } catch (Exception ex) { - log += $"Error Registering Assemblies - {ex.Message}
"; + log += $"
Error Registering Assemblies - {ex.Message}
"; } return log; From b061d4593ff9393adf66f0399e362de163fa4b9d Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 18 Feb 2025 22:02:25 +0800 Subject: [PATCH 08/60] Fix #5103: return a copy of the assembly list. --- Oqtane.Server/Controllers/InstallationController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 207d082c..0ad80eb8 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -171,7 +171,8 @@ namespace Oqtane.Controllers } } return assemblyList; - }); + }).ToList(); + } // GET api//load?list=x,y From 5e147afb9f35cf11817cb1b284515ba3b87684b2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 18 Feb 2025 09:12:26 -0500 Subject: [PATCH 09/60] clean up scheduled jobs which have been uninstalled --- Oqtane.Server/Repository/JobRepository.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Oqtane.Server/Repository/JobRepository.cs b/Oqtane.Server/Repository/JobRepository.cs index 37b38521..6d56c5c3 100644 --- a/Oqtane.Server/Repository/JobRepository.cs +++ b/Oqtane.Server/Repository/JobRepository.cs @@ -22,6 +22,14 @@ namespace Oqtane.Repository { return _cache.GetOrCreate("jobs", entry => { + // remove any jobs which have been uninstalled + foreach (var job in _db.Job.ToList()) + { + if (Type.GetType(job.JobType) == null) + { + DeleteJob(job.JobId); + } + } entry.SlidingExpiration = TimeSpan.FromMinutes(30); return _db.Job.ToList(); }); From f158a222f42f172b74163d2d149842103870f006 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 18 Feb 2025 11:35:38 -0500 Subject: [PATCH 10/60] improve HostedServiceBase so that scheduled jobs can be registered during installation --- .../Infrastructure/Jobs/HostedServiceBase.cs | 253 +++++++++--------- 1 file changed, 131 insertions(+), 122 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs index be8e8c69..5c8fc04f 100644 --- a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs +++ b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -45,110 +46,140 @@ namespace Oqtane.Infrastructure protected async Task ExecuteAsync(CancellationToken stoppingToken) { - await Task.Yield(); // required so that this method does not block startup - while (!stoppingToken.IsCancellationRequested) { using (var scope = _serviceScopeFactory.CreateScope()) { + IConfigurationRoot _config = scope.ServiceProvider.GetRequiredService(); ILogger _filelogger = scope.ServiceProvider.GetRequiredService>(); - try + // if framework is installed + if (IsInstalled(_config)) { - var jobs = scope.ServiceProvider.GetRequiredService(); - var jobLogs = scope.ServiceProvider.GetRequiredService(); - var tenantRepository = scope.ServiceProvider.GetRequiredService(); - var tenantManager = scope.ServiceProvider.GetRequiredService(); - - // get name of job - string jobType = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName); - - // load jobs and find current job - Job job = jobs.GetJobs().Where(item => item.JobType == jobType).FirstOrDefault(); - if (job != null && job.IsEnabled && !job.IsExecuting) + try { - // get next execution date - DateTime NextExecution; - if (job.NextExecution == null) + var jobs = scope.ServiceProvider.GetRequiredService(); + + // get name of job + string jobTypeName = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName); + + // load jobs and find current job + Job job = jobs.GetJobs().Where(item => item.JobType == jobTypeName).FirstOrDefault(); + + if (job == null) { - if (job.StartDate != null) + // auto registration + job = new Job { JobType = jobTypeName }; + + // optional HostedServiceBase properties + var jobType = Type.GetType(jobTypeName); + var jobObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, jobType) as HostedServiceBase; + if (jobObject.Name != "") { - NextExecution = job.StartDate.Value; + job.Name = jobObject.Name; } else { - NextExecution = DateTime.UtcNow; - } - } - else - { - NextExecution = job.NextExecution.Value; - } - - // determine if the job should be run - if (NextExecution <= DateTime.UtcNow && (job.EndDate == null || job.EndDate >= DateTime.UtcNow)) - { - // update the job to indicate it is running - job.IsExecuting = true; - jobs.UpdateJob(job); - - // create a job log entry - JobLog log = new JobLog(); - log.JobId = job.JobId; - log.StartDate = DateTime.UtcNow; - log.FinishDate = null; - log.Succeeded = false; - log.Notes = ""; - log = jobLogs.AddJobLog(log); - - // execute the job - try - { - var notes = ""; - foreach (var tenant in tenantRepository.GetTenants()) - { - // set tenant and execute job - tenantManager.SetTenant(tenant.TenantId); - notes += ExecuteJob(scope.ServiceProvider); - notes += await ExecuteJobAsync(scope.ServiceProvider); - } - log.Notes = notes; - log.Succeeded = true; - } - catch (Exception ex) - { - log.Notes = ex.Message; - log.Succeeded = false; - } - - // update the job log - log.FinishDate = DateTime.UtcNow; - jobLogs.UpdateJobLog(log); - - // update the job - job.NextExecution = CalculateNextExecution(NextExecution, job); - if (job.Frequency == "O") // one time - { - job.EndDate = DateTime.UtcNow; - job.NextExecution = null; + job.Name = Utilities.GetTypeName(job.JobType); } + job.Frequency = jobObject.Frequency; + job.Interval = jobObject.Interval; + job.StartDate = jobObject.StartDate; + job.EndDate = jobObject.EndDate; + job.RetentionHistory = jobObject.RetentionHistory; + job.IsEnabled = jobObject.IsEnabled; + job.IsStarted = true; job.IsExecuting = false; - jobs.UpdateJob(job); + job.NextExecution = null; - // trim the job log - List logs = jobLogs.GetJobLogs().Where(item => item.JobId == job.JobId) - .OrderByDescending(item => item.JobLogId).ToList(); - for (int i = logs.Count; i > job.RetentionHistory; i--) + job = jobs.AddJob(job); + } + + if (job != null && job.IsEnabled && !job.IsExecuting) + { + var jobLogs = scope.ServiceProvider.GetRequiredService(); + var tenantRepository = scope.ServiceProvider.GetRequiredService(); + var tenantManager = scope.ServiceProvider.GetRequiredService(); + + // get next execution date + DateTime NextExecution; + if (job.NextExecution == null) { - jobLogs.DeleteJobLog(logs[i - 1].JobLogId); + if (job.StartDate != null) + { + NextExecution = job.StartDate.Value; + } + else + { + NextExecution = DateTime.UtcNow; + } + } + else + { + NextExecution = job.NextExecution.Value; + } + + // determine if the job should be run + if (NextExecution <= DateTime.UtcNow && (job.EndDate == null || job.EndDate >= DateTime.UtcNow)) + { + // update the job to indicate it is running + job.IsExecuting = true; + jobs.UpdateJob(job); + + // create a job log entry + JobLog log = new JobLog(); + log.JobId = job.JobId; + log.StartDate = DateTime.UtcNow; + log.FinishDate = null; + log.Succeeded = false; + log.Notes = ""; + log = jobLogs.AddJobLog(log); + + // execute the job + try + { + var notes = ""; + foreach (var tenant in tenantRepository.GetTenants()) + { + // set tenant and execute job + tenantManager.SetTenant(tenant.TenantId); + notes += ExecuteJob(scope.ServiceProvider); + notes += await ExecuteJobAsync(scope.ServiceProvider); + } + log.Notes = notes; + log.Succeeded = true; + } + catch (Exception ex) + { + log.Notes = ex.Message; + log.Succeeded = false; + } + + // update the job log + log.FinishDate = DateTime.UtcNow; + jobLogs.UpdateJobLog(log); + + // update the job + job.NextExecution = CalculateNextExecution(NextExecution, job); + if (job.Frequency == "O") // one time + { + job.EndDate = DateTime.UtcNow; + job.NextExecution = null; + } + job.IsExecuting = false; + jobs.UpdateJob(job); + + // trim the job log + List logs = jobLogs.GetJobLogs().Where(item => item.JobId == job.JobId) + .OrderByDescending(item => item.JobLogId).ToList(); + for (int i = logs.Count; i > job.RetentionHistory; i--) + { + jobLogs.DeleteJobLog(logs[i - 1].JobLogId); + } } } } - } - catch (Exception ex) - { - // can occur during the initial installation because the database has not yet been created - if (!ex.Message.Contains("No database provider has been configured for this DbContext")) + catch (Exception ex) { _filelogger.LogError(Utilities.LogMessage(this, $"An Error Occurred Executing Scheduled Job: {Name} - {ex}")); } @@ -208,55 +239,28 @@ namespace Oqtane.Infrastructure { using (var scope = _serviceScopeFactory.CreateScope()) { + IConfigurationRoot _config = scope.ServiceProvider.GetRequiredService(); ILogger _filelogger = scope.ServiceProvider.GetRequiredService>(); try { - string jobTypeName = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName); - IJobRepository jobs = scope.ServiceProvider.GetRequiredService(); - Job job = jobs.GetJobs().Where(item => item.JobType == jobTypeName).FirstOrDefault(); - if (job != null) + if (IsInstalled(_config)) { - // reset in case this job was forcefully terminated previously - job.IsStarted = true; - job.IsExecuting = false; - jobs.UpdateJob(job); - } - else - { - // auto registration - job will not run on initial installation due to no DBContext but will run after restart - job = new Job { JobType = jobTypeName }; - - // optional HostedServiceBase properties - var jobType = Type.GetType(jobTypeName); - var jobObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, jobType) as HostedServiceBase; - if (jobObject.Name != "") + string jobTypeName = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName); + IJobRepository jobs = scope.ServiceProvider.GetRequiredService(); + Job job = jobs.GetJobs().Where(item => item.JobType == jobTypeName).FirstOrDefault(); + if (job != null) { - job.Name = jobObject.Name; + // reset in case this job was forcefully terminated previously + job.IsStarted = true; + job.IsExecuting = false; + jobs.UpdateJob(job); } - else - { - job.Name = Utilities.GetTypeName(job.JobType); - } - job.Frequency = jobObject.Frequency; - job.Interval = jobObject.Interval; - job.StartDate = jobObject.StartDate; - job.EndDate = jobObject.EndDate; - job.RetentionHistory = jobObject.RetentionHistory; - job.IsEnabled = jobObject.IsEnabled; - job.IsStarted = true; - job.IsExecuting = false; - job.NextExecution = null; - jobs.AddJob(job); } } catch (Exception ex) { - // can occur during the initial installation because the database has not yet been created - if (!ex.Message.Contains("No database provider has been configured for this DbContext")) - { - _filelogger.LogError(Utilities.LogMessage(this, $"An Error Occurred Starting Scheduled Job: {Name} - {ex}")); - } + _filelogger.LogError(Utilities.LogMessage(this, $"An Error Occurred Starting Scheduled Job: {Name} - {ex}")); } } @@ -314,6 +318,11 @@ namespace Oqtane.Infrastructure } } + private bool IsInstalled(IConfigurationRoot config) + { + return !string.IsNullOrEmpty(config.GetConnectionString(SettingKeys.ConnectionStringKey)); + } + public void Dispose() { _cancellationTokenSource.Cancel(); From 101ededd892459dc31101d809b200a9aa6425be9 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 18 Feb 2025 11:50:36 -0500 Subject: [PATCH 11/60] remove warning message related to no jobs being registered --- Oqtane.Client/Modules/Admin/Jobs/Index.razor | 4 ---- Oqtane.Client/Resources/Modules/Admin/Jobs/Index.resx | 3 --- 2 files changed, 7 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Index.razor b/Oqtane.Client/Modules/Admin/Jobs/Index.razor index 8c2dc8fe..4a2f5de6 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Index.razor @@ -55,10 +55,6 @@ else protected override async Task OnInitializedAsync() { await GetJobs(); - if (_jobs.Count == 0) - { - AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning); - } } private async Task GetJobs() diff --git a/Oqtane.Client/Resources/Modules/Admin/Jobs/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Jobs/Index.resx index 60e347c5..53326062 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Jobs/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Jobs/Index.resx @@ -180,9 +180,6 @@ Execute Once - - Please Note That After An Initial Installation You Must <a href={0}>Restart</a> The Application In Order To Activate The Default Scheduled Jobs. - Refresh From 982f3b1943f1a384b8c64345e76706cf394e1416 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 22 Feb 2025 09:49:33 +0800 Subject: [PATCH 12/60] Fix #4936: add the cookie consent theme control. --- .../OqtaneServiceCollectionExtensions.cs | 1 + .../Themes/Controls/CookieConsent.resx | 132 ++++++++++++++++++ .../Services/CookieConsentService.cs | 31 ++++ .../Interfaces/ICookieConsentService.cs | 24 ++++ .../Themes/BlazorTheme/Themes/Default.razor | 2 + .../Themes/Controls/Theme/CookieConsent.razor | 33 +++++ .../Themes/OqtaneTheme/Themes/Default.razor | 1 + Oqtane.Client/UI/Interop.cs | 13 ++ .../Controllers/CookieConsentController.cs | 34 +++++ .../OqtaneServiceCollectionExtensions.cs | 1 + .../Services/CookieConsentService.cs | 38 +++++ Oqtane.Server/Startup.cs | 8 ++ Oqtane.Server/wwwroot/js/interop.js | 3 + Oqtane.Shared/Shared/Constants.cs | 1 + 14 files changed, 322 insertions(+) create mode 100644 Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx create mode 100644 Oqtane.Client/Services/CookieConsentService.cs create mode 100644 Oqtane.Client/Services/Interfaces/ICookieConsentService.cs create mode 100644 Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor create mode 100644 Oqtane.Server/Controllers/CookieConsentController.cs create mode 100644 Oqtane.Server/Services/CookieConsentService.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 457af666..8b2e87a8 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -52,6 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx new file mode 100644 index 00000000..98fa9366 --- /dev/null +++ b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + <div class="gdpr-consent-bar bg-light text-dark p-3 fixed-bottom"> + <div class="container-fluid d-flex justify-content-between align-items-center"> + <div> + By clicking "Accept", you agree us to use cookies to ensure you get the best experience on our website. + </div> + <button class="btn btn-primary" type="submit">Accept</button> + </div> + </div> + + + \ No newline at end of file diff --git a/Oqtane.Client/Services/CookieConsentService.cs b/Oqtane.Client/Services/CookieConsentService.cs new file mode 100644 index 00000000..53e7d124 --- /dev/null +++ b/Oqtane.Client/Services/CookieConsentService.cs @@ -0,0 +1,31 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Globalization; + +namespace Oqtane.Services +{ + /// + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class CookieConsentService : ServiceBase, ICookieConsentService + { + public CookieConsentService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string ApiUrl => CreateApiUrl("CookieConsent"); + + /// + public async Task CanTrackAsync() + { + return await GetJsonAsync($"{ApiUrl}/CanTrack"); + } + + public async Task CreateConsentCookieAsync() + { + var cookie = await GetStringAsync($"{ApiUrl}/CreateConsentCookie"); + return cookie ?? string.Empty; + } + } +} diff --git a/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs b/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs new file mode 100644 index 00000000..833d68a3 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs @@ -0,0 +1,24 @@ +using Oqtane.Models; +using System; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + /// + /// Service to retrieve cookie consent information. + /// + public interface ICookieConsentService + { + /// + /// Get cookie consent status + /// + /// + Task CanTrackAsync(); + + /// + /// Grant cookie consent + /// + /// + Task CreateConsentCookieAsync(); + } +} diff --git a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor index 2c684bb8..0dcf974a 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor @@ -26,9 +26,11 @@
+
+ @code { diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor new file mode 100644 index 00000000..a6612e33 --- /dev/null +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -0,0 +1,33 @@ +@namespace Oqtane.Themes.Controls +@inherits ThemeControlBase +@inject ICookieConsentService CookieConsentService +@inject IJSRuntime JSRuntime +@inject IStringLocalizer Localizer + +@if (showBanner) +{ +
+ + @((MarkupString)Convert.ToString(Localizer["ConsentBody"])) +
+} +@code { + private bool showBanner; + + protected override async Task OnInitializedAsync() + { + showBanner = !(await CookieConsentService.CanTrackAsync()); + } + + private async Task AcceptPolicy() + { + var cookieString = await CookieConsentService.CreateConsentCookieAsync(); + if (!string.IsNullOrEmpty(cookieString)) + { + var interop = new Interop(JSRuntime); + await interop.SetCookieString(cookieString); + + showBanner = false; + } + } +} \ No newline at end of file diff --git a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor index 4814ad2a..4cd232ba 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor @@ -107,6 +107,7 @@ { } + diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 575889b3..8d547da7 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -37,6 +37,19 @@ namespace Oqtane.UI } } + public Task SetCookieString(string cookieString) + { + try + { + _jsRuntime.InvokeVoidAsync("Oqtane.Interop.setCookieString", cookieString); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } + public ValueTask GetCookie(string name) { try diff --git a/Oqtane.Server/Controllers/CookieConsentController.cs b/Oqtane.Server/Controllers/CookieConsentController.cs new file mode 100644 index 00000000..7483c0b3 --- /dev/null +++ b/Oqtane.Server/Controllers/CookieConsentController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Oqtane.Models; +using Oqtane.Shared; +using System; +using System.Globalization; +using Oqtane.Infrastructure; +using Oqtane.Services; +using System.Threading.Tasks; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class CookieConsentController : Controller + { + private readonly ICookieConsentService _cookieConsentService; + + public CookieConsentController(ICookieConsentService cookieConsentService) + { + _cookieConsentService = cookieConsentService; + } + + [HttpGet("CanTrack")] + public async Task CanTrack() + { + return await _cookieConsentService.CanTrackAsync(); + } + + [HttpGet("CreateConsentCookie")] + public async Task CreateConsentCookie() + { + return await _cookieConsentService.CreateConsentCookieAsync(); + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index b04f2061..52bb3d3e 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -103,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Server/Services/CookieConsentService.cs b/Oqtane.Server/Services/CookieConsentService.cs new file mode 100644 index 00000000..afc1dbe1 --- /dev/null +++ b/Oqtane.Server/Services/CookieConsentService.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Localization; +using Oqtane.Documentation; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class ServerCookieConsentService : ICookieConsentService + { + private readonly IHttpContextAccessor _accessor; + + public ServerCookieConsentService(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public Task CanTrackAsync() + { + var consentFeature = _accessor.HttpContext?.Features.Get(); + var canTrack = consentFeature?.CanTrack ?? true; + + return Task.FromResult(canTrack); + } + + public Task CreateConsentCookieAsync() + { + var consentFeature = _accessor.HttpContext?.Features.Get(); + consentFeature?.GrantConsent(); + var cookie = consentFeature?.CreateConsentCookie() ?? string.Empty; + + return Task.FromResult(cookie); + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 8ab42dd8..b2ad5487 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -169,6 +169,13 @@ namespace Oqtane options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type }); services.TryAddSwagger(_useSwagger); + + //add cookie consent policy + services.Configure(options => + { + options.CheckConsentNeeded = context => true; + options.ConsentCookieValue = Constants.CookieConsentCookieValue; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -225,6 +232,7 @@ namespace Oqtane app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); + app.UseCookiePolicy(); if (_useSwagger) { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 719eb63e..191d9823 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -14,6 +14,9 @@ Oqtane.Interop = { } document.cookie = cookieString; }, + setCookieString: function (cookieString) { + document.cookie = cookieString; + }, getCookie: function (name) { name = name + "="; var decodedCookie = decodeURIComponent(document.cookie); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 024fc1a7..b4ef8379 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -91,6 +91,7 @@ namespace Oqtane.Shared public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"; public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg=="; + public const string CookieConsentCookieValue = "true"; // Obsolete constants const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames"; From bf308dd13d76139ff37f691d7b79e0d5c0cd0be0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 24 Feb 2025 22:32:19 +0800 Subject: [PATCH 13/60] enable child component of cookie consent control. --- .../Themes/Controls/Theme/CookieConsent.razor | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index a6612e33..4d489b14 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -8,12 +8,22 @@ {
- @((MarkupString)Convert.ToString(Localizer["ConsentBody"])) + @if (ChildContent != null) + { + @ChildContent + } + else + { + @((MarkupString)Convert.ToString(Localizer["ConsentBody"])) + }
} @code { private bool showBanner; + [Parameter] + public RenderFragment ChildContent { get; set; } = null; + protected override async Task OnInitializedAsync() { showBanner = !(await CookieConsentService.CanTrackAsync()); From 659950996deedbb402b65d3ca0e3bf43c6b06cb7 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 24 Feb 2025 16:15:35 -0500 Subject: [PATCH 14/60] remove IJSRuntime reference as it was causing a compilation warning --- Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index 4d489b14..e7bfdc7b 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -1,7 +1,6 @@ @namespace Oqtane.Themes.Controls @inherits ThemeControlBase @inject ICookieConsentService CookieConsentService -@inject IJSRuntime JSRuntime @inject IStringLocalizer Localizer @if (showBanner) From b47bf40e8ff8971f51acac3a93f76aa438b0adb8 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Feb 2025 11:36:47 +0800 Subject: [PATCH 15/60] update the cookie consent control. --- .../Themes/Controls/CookieConsent.resx | 19 ++++------ .../Themes/OqtaneTheme/ThemeSettings.resx | 8 +++- .../Themes/BlazorTheme/Themes/Default.razor | 2 +- .../Themes/Controls/Theme/CookieConsent.razor | 30 ++++++++++----- .../Themes/OqtaneTheme/Themes/Default.razor | 10 +++-- .../OqtaneTheme/Themes/ThemeSettings.razor | 28 ++++++++++++-- .../SiteTemplates/AdminSiteTemplate.cs | 27 +++++++++++++ .../Infrastructure/UpgradeManager.cs | 38 +++++++++++++++++++ Oqtane.Shared/Shared/Constants.cs | 4 +- 9 files changed, 134 insertions(+), 32 deletions(-) diff --git a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx index 98fa9366..33700f00 100644 --- a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx +++ b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx @@ -117,16 +117,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - <div class="gdpr-consent-bar bg-light text-dark p-3 fixed-bottom"> - <div class="container-fluid d-flex justify-content-between align-items-center"> - <div> - By clicking "Accept", you agree us to use cookies to ensure you get the best experience on our website. - </div> - <button class="btn btn-primary" type="submit">Accept</button> - </div> - </div> - + + Apply + + + By clicking "Accept", you agree us to use cookies to ensure you get the best experience on our website. + + + Privacy \ No newline at end of file diff --git a/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx b/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx index 95f9ade1..ec0c9bbb 100644 --- a/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx +++ b/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + <p>This privacy policy ("policy") will help you understand how [COMPANY] ("us", "we", "our") uses and protects the data you provide to us when you visit and use this website.</p> + +<p>We reserve the right to change this policy at any time. We shall let our users know of these changes through electronic mail. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page.</p> + +<h2>What User Data We Collect</h2> + +<p>When you visit this website, we may collect the following data: your IP address, your contact information and email address, other information such as interests and preferences.</p> + +<h2>Why We Collect Your Data</h2> + +<p>We are collecting your data for several reasons: to better understand your needs, to improve our products and services, to send you promotional emails containing the information we think you will find interesting, to customize our website according to your online behavior and personal preferences.</p> + +<h2>Safeguarding and Securing the Data</h2> + +<p>[COMPANY] is committed to securing your data and keeping it confidential. [COMPANY] has done everything in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.</p> + +<h2>Our Cookie Policy</h2> + +<p>Once you agree to allow our website to use cookies, you also agree to allow us to use the data it collects regarding your online behavior (analyze web traffic, web pages you visit and spend the most time on, etc...).</p> + +<p>The data we collect by using cookies is used to customize our website to your needs.</p> + +<p>Please note that cookies don't allow us to gain access to your computer in any way. They are strictly used to monitor which pages you find useful and which you do not so that we can provide a better experience for you.</p> + +<p>If you want to disable or remove cookies, you can do so by accessing the settings of your internet browser.</p> + +<h2>Links to Other Websites</h2> + +<p>Our website contains links that lead to other websites. If you click on these links [COMPANY] is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of the website you go to from our website.</p> + +<h2>Restricting the Collection of your Personal Data</h2> + +<p>At some point, you might wish to restrict the use and collection of your personal data. If you previously agreed to share your information with us, feel free to contact us via email and we will be more than happy to change this for you.</p> + +<p>[COMPANY] will not lease, sell or distribute your personal information to any third parties, unless we have your permission. Your personal information will only be used when we need to send you promotional materials if you agree to this privacy policy.</p> + + + <p>Please read these terms and conditions carefully before using this website operated by [COMPANY] ("us", "we", "our").</p> + +<h2>Conditions of Use</h2> + +<p>By using this website, you certify that you have read and reviewed this Agreement and that you agree to comply with its terms. If you do not want to be bound by the terms of this Agreement, you are advised to stop using the website accordingly. [COMPANY] only grants use and access of this website, its products, and its services to those who have accepted its terms.</p> + +<h2>Privacy Policy</h2> + +<p>Before you continue using our website, we advise you to read our <a href="/privacy">privacy policy</a> regarding our user data collection. It will help you better understand our practices.</p> + +<h2>Intellectual Property</h2> + +<p>You agree that all materials, products, and services provided on this website are the property of [COMPANY], its affiliates, directors, officers, employees, agents, suppliers, or licensors including all copyrights, trade secrets, trademarks, patents, and other intellectual property. You also agree that you will not reproduce or redistribute the [COMPANY]’s intellectual property in any way, including electronic, digital, or new trademark registrations.</p> + +<p>You grant [COMPANY] a royalty-free and non-exclusive license to display, use, copy, transmit, and broadcast the content you upload and publish. For issues regarding intellectual property claims, you should contact us in order to come to an agreement.</p> + +<h2>User Accounts</h2> + +<p>As a user of this website, you may be asked to register with us and provide private information. You are responsible for ensuring the accuracy of this information, and you are responsible for maintaining the safety and security of your identifying information.</p> + +<p>You are also responsible for all activities that occur under your account or password. If you think there are any possible issues regarding the security of your account on the website, inform us immediately so we may address them accordingly.</p> + +<p>We reserve all rights to terminate accounts, edit or remove content and cancel orders at our sole discretion.</p> + +<h2>Applicable Law</h2> + +<p>By using this website, you agree that the laws of [LOCATION], without regard to principles of conflict laws, will govern these terms and conditions, or any dispute of any sort that might come between [COMPANY] and you, or its business partners and associates.</p> + +<h2>Disputes</h2> + +<p>Any dispute related in any way to your use of this website or to products you purchase from us shall be arbitrated by state or federal court [your location] and you consent to exclusive jurisdiction and venue of such courts.</p> + +<h2>Indemnification</h2> + +<p>You agree to indemnify [COMPANY] and its affiliates and hold [COMPANY] harmless against legal claims and demands that may arise from your use or misuse of our services. We reserve the right to select our own legal counsel.</p> + +<h2>Limitation on Liability</h2> + +<p>[COMPANY] is not liable for any damages that may occur to you as a result of your misuse of our website. [COMPANY] reserves the right to edit, modify, and change this Agreement at any time. We shall let our users know of these changes through electronic mail. This Agreement is an understanding between [COMPANY] and the user, and this supersedes and replaces all prior agreements regarding the use of this website.</p> + + + \ No newline at end of file diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 7e99421a..25dc674b 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -33,8 +33,8 @@ namespace Oqtane.Shared public const string PageManagementModule = "Oqtane.Modules.Admin.Pages, Oqtane.Client"; public const string ErrorModule = "Oqtane.Modules.Admin.Error.{Action}, Oqtane.Client"; - public const string AdminSiteTemplate = "Oqtane.SiteTemplates.AdminSiteTemplate, Oqtane.Server"; - public const string DefaultSiteTemplate = "Oqtane.SiteTemplates.DefaultSiteTemplate, Oqtane.Server"; + public const string AdminSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.AdminSiteTemplate, Oqtane.Server"; + public const string DefaultSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.DefaultSiteTemplate, Oqtane.Server"; public static readonly string[] DefaultHostModuleTypes = new[] { "Upgrade", "Themes", "SystemInfo", "Sql", "Sites", "ModuleDefinitions", "Logs", "Jobs", "ModuleCreator" }; From 8518476c87e67f9bd398c20beb2af04b4f7ab552 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 3 Mar 2025 13:36:32 -0500 Subject: [PATCH 33/60] add terms to upgrademanager --- .../Infrastructure/UpgradeManager.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 75e63ac3..17fa8fbd 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Oqtane.Infrastructure.SiteTemplates; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; @@ -462,6 +464,8 @@ namespace Oqtane.Infrastructure private void Upgrade_6_1_1(Tenant tenant, IServiceScope scope) { + var localizer = scope.ServiceProvider.GetRequiredService>(); + var pageTemplates = new List { new PageTemplate @@ -486,7 +490,33 @@ namespace Oqtane.Infrastructure new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, - Content = "

By using our website, you agree to this privacy policy. We value your privacy and are committed to protecting your personal information. This policy outlines how we collect, use, and safeguard your data when you visit our website or use our services.

" + Content = localizer["Privacy"] + } + } + }, + new PageTemplate + { + Name = "Terms", + Parent = "", + Path = "terms", + Icon = Icons.List, + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List + { + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Terms & Conditions", Pane = PaneNames.Default, + PermissionList = new List { + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + Content = localizer["Terms"] } } } From f315ad1ce9e5f5abc21afe5ce898f85c99f3e158 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 3 Mar 2025 15:37:31 -0500 Subject: [PATCH 34/60] modify cookie consent text --- .../Themes/Controls/CookieConsent.resx | 4 +-- .../Themes/Controls/Theme/CookieConsent.razor | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx index 6beb4805..1e5fc1e2 100644 --- a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx +++ b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx @@ -120,8 +120,8 @@ Confirm - - I agree to using cookies to provide the best user experience for this site. + + I agree to use cookies to provide the best possible user experience for this site. I understand that I can change these preferences at any time. Privacy diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index 1ff5406b..92b862f0 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -12,24 +12,31 @@ @if (_showBanner) { -
-
- @((MarkupString)Convert.ToString(Localizer["ConsentDescription"])) +
+
@if (PageState.RenderMode == RenderModes.Static) { - + } else { - + } + @((MarkupString)Convert.ToString(Localizer["ConsentNotice"]))
-
- - @if (ShowPrivacyLink) - { - @((MarkupString)Convert.ToString(Localizer["Privacy"])) - } +
+
+
+ +
+ @if (ShowPrivacyLink) + { + + + } +
} From 5b239179403b234ea97dc747eb8e4e01a058a356 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 3 Mar 2025 16:49:28 -0500 Subject: [PATCH 35/60] add nonce support --- Oqtane.Server/wwwroot/js/reload.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 1b058f4e..528a8551 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -65,6 +65,7 @@ function injectScript(script) { newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); } } + newScript.nonce = script.nonce; // must be referenced explicitly newScript.innerHTML = script.innerHTML; // dynamically injected scripts cannot be async or deferred From 5e2092c6d4affa6be9db02acc8516542d8a34a63 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 4 Mar 2025 23:31:21 +0800 Subject: [PATCH 36/60] improve the styles in middle screen size. --- .../Themes/BlazorTheme/Themes/Default.razor | 8 +- .../Themes/Controls/Theme/CookieConsent.razor | 59 ++++++------- .../Themes/Controls/Theme/Search.razor | 2 +- .../Oqtane.Themes.BlazorTheme/Theme.css | 17 ++-- .../Oqtane.Themes.OqtaneTheme/Theme.css | 82 ++++++++++--------- 5 files changed, 87 insertions(+), 81 deletions(-) diff --git a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor index 0dcf974a..1622c5e7 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor @@ -1,11 +1,6 @@ @namespace Oqtane.Themes.BlazorTheme @inherits ThemeBase - - -
+
diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index 92b862f0..db39b44f 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -12,30 +12,32 @@ @if (_showBanner) { -
-
- @if (PageState.RenderMode == RenderModes.Static) - { - - } - else - { - - } - @((MarkupString)Convert.ToString(Localizer["ConsentNotice"])) -
-
-
-
- -
- @if (ShowPrivacyLink) +
+
+
+ @if (PageState.RenderMode == RenderModes.Static) { - - + } + else + { + + } + @((MarkupString)Convert.ToString(Localizer["ConsentNotice"])) +
+
+
+
+ +
+ @if (ShowPrivacyLink) + { + + + } +
@@ -43,7 +45,7 @@
- @if(_showBanner) + @if (_showBanner) {
- +
+ + + +
+
+
+ + +@code { + private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization + + private ElementReference form; + private bool validated = false; + + private string _dynamictokens; + + protected override void OnInitialized() + { + try + { + _dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"); + } + catch (Exception ex) + { + AddModuleMessage(ex.Message, MessageType.Error); + } + } + + public async Task UpdateSettings() + { + try + { + var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); + settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens); + await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); + } + catch (Exception ex) + { + AddModuleMessage(ex.Message, MessageType.Error); + } + } +} diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index fe40ddf6..b97b229b 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using Microsoft.JSInterop; using System.Linq; using System.Dynamic; +using System.Reflection; namespace Oqtane.Modules { @@ -35,7 +36,7 @@ namespace Oqtane.Modules protected PageState PageState { get; set; } [CascadingParameter] - protected Module ModuleState { get; set; } + protected Models.Module ModuleState { get; set; } [Parameter] public RenderModeBoundary RenderModeBoundary { get; set; } @@ -413,6 +414,79 @@ namespace Oqtane.Modules await interop.ScrollTo(0, 0, "smooth"); } + public string ReplaceTokens(string content) + { + return ReplaceTokens(content, null); + } + + public string ReplaceTokens(string content, object obj) + { + var tokens = new List(); + var pos = content.IndexOf("["); + if (pos != -1) + { + if (content.IndexOf("]", pos) != -1) + { + var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1); + if (token.Contains(":")) + { + tokens.Add(token.Substring(1, token.Length - 2)); + } + } + pos = content.IndexOf("[", pos + 1); + } + if (tokens.Count != 0) + { + foreach (string token in tokens) + { + var segments = token.Split(":"); + if (segments.Length >= 2 && segments.Length <= 3) + { + var objectName = string.Join(":", segments, 0, segments.Length - 1); + var propertyName = segments[segments.Length - 1]; + var propertyValue = ""; + + switch (objectName) + { + case "ModuleState": + propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString(); + break; + case "PageState": + propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString(); + break; + case "PageState:Alias": + propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString(); + break; + case "PageState:Site": + propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString(); + break; + case "PageState:Page": + propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString(); + break; + case "PageState:User": + propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString(); + break; + case "PageState:Route": + propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString(); + break; + default: + if (obj != null && obj.GetType().Name == objectName) + { + propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString(); + } + break; + } + if (propertyValue != null) + { + content = content.Replace("[" + token + "]", propertyValue); + } + + } + } + } + return content; + } + // logging methods public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args) { diff --git a/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx new file mode 100644 index 00000000..68a539c0 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Do you wish to allow tokens to be dynamically replaced? Please note that this will affect the performance of your site. + + + Dynamic Tokens? + + \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index 6ac4d9b3..7727e9aa 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -199,6 +199,9 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, Content = _localizer["Privacy"] } } @@ -226,6 +229,9 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, Content = _localizer["Terms"] } } diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index e2e46ff8..409ab59c 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -490,6 +490,9 @@ namespace Oqtane.Infrastructure new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, Content = localizer["Privacy"] } } @@ -516,6 +519,9 @@ namespace Oqtane.Infrastructure new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, Content = localizer["Terms"] } } diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 36495c8f..10de3576 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -9,6 +9,7 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Modules; +using Oqtane.Modules.Admin.Modules; using Oqtane.Shared; using Module = Oqtane.Models.Module; @@ -25,6 +26,7 @@ namespace Oqtane.Repository private readonly IPageModuleRepository _pageModuleRepository; private readonly IModuleDefinitionRepository _moduleDefinitionRepository; private readonly IThemeRepository _themeRepository; + private readonly ISettingRepository _settingRepository; private readonly IServiceProvider _serviceProvider; private readonly IConfigurationRoot _config; private readonly IServerStateManager _serverState; @@ -32,8 +34,8 @@ namespace Oqtane.Repository private static readonly object _lock = new object(); public SiteRepository(IDbContextFactory factory, IRoleRepository roleRepository, IProfileRepository profileRepository, IFolderRepository folderRepository, IPageRepository pageRepository, - IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, IServiceProvider serviceProvider, - IConfigurationRoot config, IServerStateManager serverState, ILogManager logger) + IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, ISettingRepository settingRepository, + IServiceProvider serviceProvider, IConfigurationRoot config, IServerStateManager serverState, ILogManager logger) { _factory = factory; _roleRepository = roleRepository; @@ -44,6 +46,7 @@ namespace Oqtane.Repository _pageModuleRepository = pageModuleRepository; _moduleDefinitionRepository = moduleDefinitionRepository; _themeRepository = themeRepository; + _settingRepository = settingRepository; _serviceProvider = serviceProvider; _config = config; _serverState = serverState; @@ -391,6 +394,7 @@ namespace Oqtane.Repository { _logger.Log(LogLevel.Information, "Site Template", LogFunction.Update, "Page Updated {Page}", page); } + UpdateSettings(EntityNames.Page, page.PageId, pageTemplate.Settings); } } else @@ -401,6 +405,7 @@ namespace Oqtane.Repository { _logger.Log(LogLevel.Information, "Site Template", LogFunction.Create, "Page Added {Page}", page); } + UpdateSettings(EntityNames.Page, page.PageId, pageTemplate.Settings); } } catch (Exception ex) @@ -457,6 +462,7 @@ namespace Oqtane.Repository { _logger.Log(LogLevel.Information, "Site Template", LogFunction.Update, "Page Module Updated {PageModule}", pageModule); } + UpdateSettings(EntityNames.Module, pageModule.Module.ModuleId, pageTemplateModule.Settings); } else { @@ -475,6 +481,7 @@ namespace Oqtane.Repository { _logger.Log(LogLevel.Information, "Site Template", LogFunction.Create, "Page Module Added {PageModule}", pageModule); } + UpdateSettings(EntityNames.Module, pageModule.Module.ModuleId, pageTemplateModule.Settings); } } @@ -522,5 +529,25 @@ namespace Oqtane.Repository } } } + + private void UpdateSettings(string entityName, int entityId, List templateSettings) + { + foreach (var templateSetting in templateSettings) + { + var setting = _settingRepository.GetSetting(entityName, entityId, templateSetting.SettingName); + if (setting == null) + { + templateSetting.EntityName = entityName; + templateSetting.EntityId = entityId; + _settingRepository.AddSetting(templateSetting); + } + else + { + setting.SettingValue = templateSetting.SettingValue; + setting.IsPrivate = templateSetting.IsPrivate; + _settingRepository.UpdateSetting(setting); + } + } + } } } diff --git a/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx b/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx index 07a8937e..c13f4563 100644 --- a/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx +++ b/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx @@ -118,9 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - <p>This privacy policy ("policy") will help you understand how [COMPANY] ("us", "we", "our") uses and protects the data you provide to us when you visit and use this website.</p> + <p>This privacy policy ("policy") will help you understand how [PageState:Site:Name] ("us", "we", "our") uses and protects the data you provide to us when you visit and use this website.</p> -<p>We reserve the right to change this policy at any time. We shall let our users know of these changes through electronic mail. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page.</p> +<p>We reserve the right to change this policy at any time. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page.</p> <h2>What User Data We Collect</h2> @@ -132,7 +132,7 @@ <h2>Safeguarding and Securing the Data</h2> -<p>[COMPANY] is committed to securing your data and keeping it confidential. [COMPANY] has done everything in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.</p> +<p>[PageState:Site:Name] is committed to securing your data and keeping it confidential. [PageState:Site:Name] has done everything in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.</p> <h2>Our Cookie Policy</h2> @@ -146,20 +146,20 @@ <h2>Links to Other Websites</h2> -<p>Our website contains links that lead to other websites. If you click on these links [COMPANY] is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of the website you go to from our website.</p> +<p>Our website contains links that lead to other websites. If you click on these links [PageState:Site:Name] is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of any website you navigate to from our website.</p> <h2>Restricting the Collection of your Personal Data</h2> <p>At some point, you might wish to restrict the use and collection of your personal data. If you previously agreed to share your information with us, feel free to contact us via email and we will be more than happy to change this for you.</p> -<p>[COMPANY] will not lease, sell or distribute your personal information to any third parties, unless we have your permission. Your personal information will only be used when we need to send you promotional materials if you agree to this privacy policy.</p> +<p>[PageState:Site:Name] will not lease, sell or distribute your personal information to any third parties, unless we have your permission. Your personal information will only be used when we need to send you promotional materials if you agree to this privacy policy.</p> - <p>Please read these terms and conditions carefully before using this website operated by [COMPANY] ("us", "we", "our").</p> + <p>Please read these terms and conditions carefully before using this website operated by [PageState:Site:Name] ("us", "we", "our").</p> <h2>Conditions of Use</h2> -<p>By using this website, you certify that you have read and reviewed this Agreement and that you agree to comply with its terms. If you do not want to be bound by the terms of this Agreement, you are advised to stop using the website accordingly. [COMPANY] only grants use and access of this website, its products, and its services to those who have accepted its terms.</p> +<p>By using this website, you certify that you have read and reviewed this Agreement and that you agree to comply with its terms. If you do not want to be bound by the terms of this Agreement, you are advised to stop using the website accordingly. [PageState:Site:Name] only grants use and access of this website, its products, and its services to those who have accepted its terms.</p> <h2>Privacy Policy</h2> @@ -167,9 +167,9 @@ <h2>Intellectual Property</h2> -<p>You agree that all materials, products, and services provided on this website are the property of [COMPANY], its affiliates, directors, officers, employees, agents, suppliers, or licensors including all copyrights, trade secrets, trademarks, patents, and other intellectual property. You also agree that you will not reproduce or redistribute the [COMPANY]’s intellectual property in any way, including electronic, digital, or new trademark registrations.</p> +<p>You agree that all materials, products, and services provided on this website are the property of [PageState:Site:Name], its affiliates, directors, officers, employees, agents, suppliers, or licensors including all copyrights, trade secrets, trademarks, patents, and other intellectual property. You also agree that you will not reproduce or redistribute the [PageState:Site:Name]’s intellectual property in any way, including electronic, digital, or new trademark registrations.</p> -<p>You grant [COMPANY] a royalty-free and non-exclusive license to display, use, copy, transmit, and broadcast the content you upload and publish. For issues regarding intellectual property claims, you should contact us in order to come to an agreement.</p> +<p>You grant [PageState:Site:Name] a royalty-free and non-exclusive license to display, use, copy, transmit, and broadcast the content you upload and publish. For issues regarding intellectual property claims, you should contact us in order to come to an agreement.</p> <h2>User Accounts</h2> @@ -181,19 +181,19 @@ <h2>Applicable Law</h2> -<p>By using this website, you agree that the laws of [LOCATION], without regard to principles of conflict laws, will govern these terms and conditions, or any dispute of any sort that might come between [COMPANY] and you, or its business partners and associates.</p> +<p>By using this website, you agree that the laws of the jurisdiction associated to [PageState:Site:Name], without regard to principles of conflict laws, will govern these terms and conditions, or any dispute of any sort that might come between [PageState:Site:Name] and you, or its business partners and associates.</p> <h2>Disputes</h2> -<p>Any dispute related in any way to your use of this website or to products you purchase from us shall be arbitrated by state or federal court [your location] and you consent to exclusive jurisdiction and venue of such courts.</p> +<p>Any dispute related in any way to your use of this website or to products you purchase from us shall be arbitrated by a court of law and you consent to exclusive jurisdiction and venue of such courts.</p> <h2>Indemnification</h2> -<p>You agree to indemnify [COMPANY] and its affiliates and hold [COMPANY] harmless against legal claims and demands that may arise from your use or misuse of our services. We reserve the right to select our own legal counsel.</p> +<p>You agree to indemnify [PageState:Site:Name] and its affiliates and hold [PageState:Site:Name] harmless against legal claims and demands that may arise from your use or misuse of our services. We reserve the right to select our own legal counsel.</p> <h2>Limitation on Liability</h2> -<p>[COMPANY] is not liable for any damages that may occur to you as a result of your misuse of our website. [COMPANY] reserves the right to edit, modify, and change this Agreement at any time. We shall let our users know of these changes through electronic mail. This Agreement is an understanding between [COMPANY] and the user, and this supersedes and replaces all prior agreements regarding the use of this website.</p> +<p>[PageState:Site:Name] is not liable for any damages that may occur to you as a result of your misuse of our website. [PageState:Site:Name] reserves the right to edit, modify, and change this Agreement at any time. We shall let our users know of these changes through electronic mail. This Agreement is an understanding between [PageState:Site:Name] and the user, and this supersedes and replaces all prior agreements regarding the use of this website.</p> \ No newline at end of file diff --git a/Oqtane.Shared/Models/SiteTemplate.cs b/Oqtane.Shared/Models/SiteTemplate.cs index 5e449a9b..348e1834 100644 --- a/Oqtane.Shared/Models/SiteTemplate.cs +++ b/Oqtane.Shared/Models/SiteTemplate.cs @@ -35,6 +35,7 @@ namespace Oqtane.Models new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }; + Settings = new List(); PageTemplateModules = new List(); // properties used by IModule @@ -60,6 +61,7 @@ namespace Oqtane.Models public bool IsPersonalizable { get; set; } public bool IsDeleted { get; set; } public List PermissionList { get; set; } + public List Settings { get; set; } public List PageTemplateModules { get; set; } // properties used by IModule @@ -99,6 +101,7 @@ namespace Oqtane.Models new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }; + Settings = new List(); Content = ""; } @@ -109,6 +112,7 @@ namespace Oqtane.Models public string ContainerType { get; set; } public bool IsDeleted { get; set; } public List PermissionList { get; set; } + public List Settings { get; set; } public string Content { get; set; } [Obsolete("The ModulePermissions property is deprecated. Use PermissionList instead", false)] From ba1bfd1bc0a7526ab1660820b478797e9f29ae29 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Mar 2025 15:25:25 -0500 Subject: [PATCH 45/60] update based on changes suggested by @adefwebserver --- azuredeploy.json | 109 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/azuredeploy.json b/azuredeploy.json index 95728590..123ee8e2 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.1", + "contentVersion": "1.0.0.2", "parameters": { "sqlDatabaseEditionTierDtuCapacity": { "type": "string", @@ -18,7 +18,7 @@ "Standard-S12-3000-250", "Premium-P1-125-500", "Premium-P2-250-500", - "Premium-P4-500-500" , + "Premium-P4-500-500", "Premium-P6-1000-500", "Premium-P11-1750-500-1024", "Premium-P15-4000-1024", @@ -38,19 +38,19 @@ "sqlDatabaseName": { "type": "string", "metadata": { - "description": "The name of the sql databaseName. It has to be unique." + "description": "The name of the sql database. It has to be unique." } }, "sqlAdministratorLogin": { "type": "string", "metadata": { - "description": "The admin user of the SQL Server" + "description": "The admin user of the SQL Server." } }, "sqlAdministratorLoginPassword": { "type": "securestring", "metadata": { - "description": "The password of the admin user of the SQL Server" + "description": "The password of the admin user of the SQL Server." } }, "BlazorWebsiteName": { @@ -75,7 +75,10 @@ "P3", "P4" ], - "defaultValue": "B1" + "defaultValue": "B1", + "metadata": { + "description": "The SKU for the App Service Plan" + } }, "BlazorSKUCapacity": { "type": "int", @@ -101,15 +104,18 @@ "databaseEdition": "[variables('databaseEditionTierDtuCapacity')[0]]", "databaseTier": "[variables('databaseEditionTierDtuCapacity')[1]]", "databaseDtu": "[if(greater(length(variables('databaseEditionTierDtuCapacity')), 2), variables('databaseEditionTierDtuCapacity')[2], '')]", - "databaseMaxSizeGigaBytes":"[if(greater(length(variables('databaseEditionTierDtuCapacity')), 3), variables('databaseEditionTierDtuCapacity')[3], '')]", + "databaseMaxSizeGigaBytes": "[if(greater(length(variables('databaseEditionTierDtuCapacity')), 3), variables('databaseEditionTierDtuCapacity')[3], '')]", "databaseServerlessTiers": [ - "GP_S_Gen5_2" - ] + "GP_S_Gen5_2" + ] }, "resources": [ + // ------------------------------------------------------ + // SQL Server + // ------------------------------------------------------ { "type": "Microsoft.Sql/servers", - "apiVersion": "2021-11-01", + "apiVersion": "2022-05-01-preview", // Updated API version "name": "[parameters('sqlServerName')]", "location": "[parameters('location')]", "tags": { @@ -121,9 +127,12 @@ "version": "12.0" } }, + // ------------------------------------------------------ + // SQL Database (separate resource rather than subresource) + // ------------------------------------------------------ { "type": "Microsoft.Sql/servers/databases", - "apiVersion": "2021-11-01", + "apiVersion": "2022-05-01-preview", // Updated API version "name": "[format('{0}/{1}', parameters('sqlServerName'), parameters('sqlDatabaseName'))]", "location": "[parameters('location')]", "tags": { @@ -132,24 +141,40 @@ "sku": { "name": "[if(equals(variables('databaseEdition'), 'GeneralPurpose'), variables('databaseTier'), variables('databaseEdition'))]", "tier": "[variables('databaseEdition')]", - "capacity": "[if(equals(variables('databaseDtu'), ''), json('null'), int(variables('databaseDtu')))]" + "capacity": "[if(equals(variables('databaseDtu'), ''), json('null'), int(variables('databaseDtu')))]" }, - "kind": "[concat('v12.0,user,vcore',if(contains(variables('databaseServerlessTiers'),variables('databaseTier')),',serverless',''))]", + "kind": "[concat('v12.0,user,vcore', if(contains(variables('databaseServerlessTiers'), variables('databaseTier')), ',serverless', ''))]", "properties": { "edition": "[variables('databaseEdition')]", "collation": "[variables('databaseCollation')]", "maxSizeBytes": "[if(equals(variables('databaseMaxSizeGigaBytes'), ''), json('null'), mul(mul(mul(int(variables('databaseMaxSizeGigaBytes')),1024),1024),1024))]", "requestedServiceObjectiveName": "[variables('databaseTier')]" - }, "dependsOn": [ - "[resourceId('Microsoft.Sql/servers', parameters('sqlserverName'))]" + "[resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))]" ] }, + // ------------------------------------------------------ + // Transparent Data Encryption child resource + // ------------------------------------------------------ + { + "type": "Microsoft.Sql/servers/databases/transparentDataEncryption", + "apiVersion": "2021-02-01-preview", + "name": "[format('{0}/{1}/current', parameters('sqlServerName'), parameters('sqlDatabaseName'))]", + "properties": { + "state": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers/databases', parameters('sqlServerName'), parameters('sqlDatabaseName'))]" + ] + }, + // ------------------------------------------------------ + // Firewall Rule (renamed to 'AllowAllMicrosoftAzureIps') + // ------------------------------------------------------ { "type": "Microsoft.Sql/servers/firewallRules", - "apiVersion": "2021-11-01", - "name": "[format('{0}/{1}', parameters('sqlServerName'), 'AllowAllWindowsAzureIps')]", + "apiVersion": "2022-05-01-preview", // Updated API version + "name": "[format('{0}/{1}', parameters('sqlServerName'), 'AllowAllMicrosoftAzureIps')]", "properties": { "endIpAddress": "0.0.0.0", "startIpAddress": "0.0.0.0" @@ -158,26 +183,33 @@ "[resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))]" ] }, + // ------------------------------------------------------ + // App Service Plan + // ------------------------------------------------------ { - "name": "[variables('hostingPlanName')]", "type": "Microsoft.Web/serverfarms", - "location": "[resourceGroup().location]", - "apiVersion": "2022-09-01", - "dependsOn": [], + "apiVersion": "2022-03-01", // Updated API version + "name": "[variables('hostingPlanName')]", + "location": "[parameters('location')]", "tags": { "displayName": "Blazor" }, "sku": { "name": "[parameters('BlazorSKU')]", + // If you want to auto-map to certain "tier" strings, you can do so. Here we just set the capacity: "capacity": "[parameters('BlazorSKUCapacity')]" }, "properties": { "name": "[variables('hostingPlanName')]", "numberOfWorkers": 1 - } + }, + "dependsOn": [] }, + // ------------------------------------------------------ + // Web App + // ------------------------------------------------------ { - "apiVersion": "2018-02-01", + "apiVersion": "2022-03-01", // Updated API version "name": "[parameters('BlazorWebsiteName')]", "type": "Microsoft.Web/sites", "location": "[parameters('location')]", @@ -189,27 +221,46 @@ "displayName": "Website" }, "properties": { - "name": "[parameters('BlazorWebsiteName')]", "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", "siteConfig": { "webSocketsEnabled": true, - "netFrameworkVersion": "v5.0" + // Updated .NET version "v9.0" from second snippet + "netFrameworkVersion": "v9.0" } }, "resources": [ + // -------------------------------------------------- + // Source Control for your Web App + // -------------------------------------------------- { "type": "sourcecontrols", - "apiVersion": "2018-02-01", + "apiVersion": "2022-03-01", "name": "web", "location": "[parameters('location')]", "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" - //"[resourceId('Microsoft.Web/Sites/config', parameters('BlazorWebsiteName'), 'connectionstrings')]" ], "properties": { - "RepoUrl": "https://github.com/oqtane/oqtane.framework.git", + "repoUrl": "https://github.com/oqtane/oqtane.framework.git", "branch": "master", - "IsManualIntegration": true + "isManualIntegration": true + } + }, + // -------------------------------------------------- + // Connection Strings (to use FQDN) + // -------------------------------------------------- + { + "type": "config", + "apiVersion": "2022-03-01", + "name": "connectionstrings", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" + ], + "properties": { + "DefaultConnection": { + "value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('sqlDatabaseName'), ';User Id=', parameters('sqlAdministratorLogin'), '@', reference(resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))).fullyQualifiedDomainName, ';Password=', parameters('sqlAdministratorLoginPassword'), ';')]", + "type": "SQLAzure" + } } } ] From 486184b16c941222be36261cba16f3e90a7ebcce Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:36:28 -0500 Subject: [PATCH 46/60] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 468612c0..6b339b93 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,12 @@ Oqtane is being developed based on some fundamental principles which are outline [6.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0) was released on February 11, 2025 and is a minor release including 95 pull requests by 9 different contributors, pushing the total number of project commits all-time to over 6300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +# Try It Now! + [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) +[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) + # Getting Started (Version 6.x) **Installing using source code from the Dev/Master branch:** From cb8e9ee24452cc83b8ac153822b7b14b372fb2db Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:38:18 -0500 Subject: [PATCH 47/60] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b339b93..afa71e8c 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) +Automated Deployment to Microsoft Azure [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) -[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) +Automated Deployment to Monster ASP.NET [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) # Getting Started (Version 6.x) From ee2b2e3569638ffddf3bae9c1a37b9b1cdc235c1 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:40:07 -0500 Subject: [PATCH 48/60] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afa71e8c..b13f05b3 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! -Automated Deployment to Microsoft Azure [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) Microsoft's Public Cloud -Automated Deployment to Monster ASP.NET [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) + +[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) Free hosting account. No hidden fees. No credit card required. # Getting Started (Version 6.x) From cebed93abffa6efda2d98333d1c929996a9c04a4 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:42:48 -0500 Subject: [PATCH 49/60] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b13f05b3..bdd023c9 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,11 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) Microsoft's Public Cloud +Microsoft's Public Cloud (requires an Azure Tenant) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) - -[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) Free hosting account. No hidden fees. No credit card required. +A free ASP.NET hosting account. No hidden fees. No credit card required. +[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) # Getting Started (Version 6.x) From bca0866d72de9e343db67680939d620795898028 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:43:59 -0500 Subject: [PATCH 50/60] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bdd023c9..01c6113b 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! -Microsoft's Public Cloud (requires an Azure Tenant) +Microsoft's Public Cloud (requires an Azure Tenant) [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) -A free ASP.NET hosting account. No hidden fees. No credit card required. +A free ASP.NET hosting account. No hidden fees. No credit card required. [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) # Getting Started (Version 6.x) From a88ea9780f13209c337b76f962001e3b994168b2 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Mar 2025 15:45:30 -0500 Subject: [PATCH 51/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01c6113b..241ab719 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! Microsoft's Public Cloud (requires an Azure Tenant) -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fdev%2Fazuredeploy.json) A free ASP.NET hosting account. No hidden fees. No credit card required. [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) From f1771610fe9e0cbaa631e48bca422e9d9fcdf240 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 7 Mar 2025 14:15:16 -0500 Subject: [PATCH 52/60] allow site settings to be overidden at host level --- Oqtane.Server/Repository/SettingRepository.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Oqtane.Server/Repository/SettingRepository.cs b/Oqtane.Server/Repository/SettingRepository.cs index b43a1d6a..0ca28d50 100644 --- a/Oqtane.Server/Repository/SettingRepository.cs +++ b/Oqtane.Server/Repository/SettingRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; @@ -27,7 +28,7 @@ namespace Oqtane.Repository { if (IsMaster(entityName)) { - return _master.Setting.Where(item => item.EntityName == entityName); + return _master.Setting.Where(item => item.EntityName == entityName).ToList(); } else { @@ -38,13 +39,28 @@ namespace Oqtane.Repository public IEnumerable GetSettings(string entityName, int entityId) { - var settings = GetSettings(entityName); + var settings = GetSettings(entityName).ToList(); + if (entityName == EntityNames.Site) + { + // site settings can be overridden by host settings + var hostsettings = GetSettings(EntityNames.Host); + foreach (var hostsetting in hostsettings) + { + if (settings.Any(item => item.SettingName == hostsetting.SettingName)) + { + settings.First(item => item.SettingName == hostsetting.SettingName).SettingValue = hostsetting.SettingValue; + } + else + { + settings.Add(new Setting { SettingId = -1, EntityName = entityName, EntityId = entityId, SettingName = hostsetting.SettingName, SettingValue = hostsetting.SettingValue, IsPrivate = hostsetting.IsPrivate }); + } + } + } return settings.Where(item => item.EntityId == entityId); } public Setting AddSetting(Setting setting) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(setting.EntityName)) { _master.Setting.Add(setting); @@ -52,6 +68,7 @@ namespace Oqtane.Repository } else { + using var tenant = _tenantContextFactory.CreateDbContext(); tenant.Setting.Add(setting); tenant.SaveChanges(); } @@ -61,7 +78,6 @@ namespace Oqtane.Repository public Setting UpdateSetting(Setting setting) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(setting.EntityName)) { _master.Entry(setting).State = EntityState.Modified; @@ -69,6 +85,7 @@ namespace Oqtane.Repository } else { + using var tenant = _tenantContextFactory.CreateDbContext(); tenant.Entry(setting).State = EntityState.Modified; tenant.SaveChanges(); } @@ -78,33 +95,32 @@ namespace Oqtane.Repository public Setting GetSetting(string entityName, int settingId) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(entityName)) { return _master.Setting.Find(settingId); } else { + using var tenant = _tenantContextFactory.CreateDbContext(); return tenant.Setting.Find(settingId); } } public Setting GetSetting(string entityName, int entityId, string settingName) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(entityName)) { return _master.Setting.Where(item => item.EntityName == entityName && item.EntityId == entityId && item.SettingName == settingName).FirstOrDefault(); } else { + using var tenant = _tenantContextFactory.CreateDbContext(); return tenant.Setting.Where(item => item.EntityName == entityName && item.EntityId == entityId && item.SettingName == settingName).FirstOrDefault(); } } public void DeleteSetting(string entityName, int settingId) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(entityName)) { Setting setting = _master.Setting.Find(settingId); @@ -113,6 +129,7 @@ namespace Oqtane.Repository } else { + using var tenant = _tenantContextFactory.CreateDbContext(); Setting setting = tenant.Setting.Find(settingId); tenant.Setting.Remove(setting); tenant.SaveChanges(); @@ -122,7 +139,6 @@ namespace Oqtane.Repository public void DeleteSettings(string entityName, int entityId) { - using var tenant = _tenantContextFactory.CreateDbContext(); if (IsMaster(entityName)) { IEnumerable settings = _master.Setting @@ -136,6 +152,7 @@ namespace Oqtane.Repository } else { + using var tenant = _tenantContextFactory.CreateDbContext(); IEnumerable settings = tenant.Setting .Where(item => item.EntityName == entityName) .Where(item => item.EntityId == entityId); From d57132d1e46cb6a17cdd270cb864747e05ef0e4d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 7 Mar 2025 14:16:47 -0500 Subject: [PATCH 53/60] upgrade to ImageSharp 3.1.7 due to security vulnerability --- Oqtane.Server/Oqtane.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 571d6fc4..ea704ea7 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -46,7 +46,7 @@ - + From 70a3fab1ff04f9825c1bd8494f58be28975ddea1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Mar 2025 10:53:05 -0400 Subject: [PATCH 54/60] added Logout Everywhere option to User Settings --- Oqtane.Client/Modules/Admin/Users/Index.razor | 658 +++++++++--------- .../Resources/Modules/Admin/Users/Index.resx | 6 + .../Theme/ControlPanelInteractive.razor | 2 +- .../Themes/Controls/Theme/Login.razor | 1 + .../Themes/Controls/Theme/LoginBase.cs | 5 +- 5 files changed, 346 insertions(+), 326 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 8af3d381..e0d895c8 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -17,171 +17,180 @@ else { -   +   -
-   -   -   +
+   +   +   @Localizer["Username"] @Localizer["Name"] @Localizer["Email"] @Localizer["LastLoginOn"] -
- - +
+ + - - + + - - + + - - @context.User.Username - @context.User.DisplayName - @((MarkupString)string.Format("{1}", @context.User.Email, @context.User.Email)) - @((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "") - -
+ + @context.User.Username + @context.User.DisplayName + @((MarkupString)string.Format("{1}", @context.User.Email, @context.User.Email)) + @((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "") + +
-
-
-
- -
- -
-
- @if (_providertype != "") - { -
- -
- -
-
- } - else - { -
- -
- -
-
- } - @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { -
- -
- -
-
-
- -
- -
-
-
+
+
+
+ +
+ +
+
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + @if (_providertype != "") + { +
+ +
+ +
+
+ } + else + { +
+ +
+ +
+
+ } +
+ +
+ +
+
+
+ +
+ +
+
+
-
- -
-
-
+
+ +
+
+
-
+
-
-
- } -
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { -
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
- -
- -
-
-
-
+ + + +
+
+
+ +
+ +
+
+ } +
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
@@ -201,77 +210,77 @@ else
- -
- - - - -
-
- @if (_providertype != "") - { -
- -
- -
-
- } - @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) - { -
- -
- -
-
-
- -
- -
-
- } - @if (_providertype == AuthenticationProviderTypes.OAuth2) - { -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- } - @if (_providertype != "") - { -
- -
- -
-
-
- -
-
- - -
-
-
+ + + +
+
+ @if (_providertype != "") + { +
+ +
+ +
+
+ } + @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) + { +
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_providertype == AuthenticationProviderTypes.OAuth2) + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_providertype != "") + { +
+ +
+ +
+
+
+ +
+
+ + +
+
+
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect) {
@@ -291,32 +300,32 @@ else
}
- -
- -
-
-
- -
- -
-
+ +
+ +
+
- -
- -
-
-
- -
- -
-
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
@@ -334,10 +343,10 @@ else
-
- -
-
+
+ +
+
@@ -346,16 +355,16 @@ else
-
- -
-
-
- -
- -
-
+
+ +
+
+
+ +
+ +
+
@@ -374,11 +383,11 @@ else
- -
- -
-
+ +
+ +
+
@@ -389,20 +398,20 @@ else
- -
- -
-
-
- -
- -
-
+ +
+ +
+
+
+ +
+ +
+
@@ -413,51 +422,51 @@ else
} - -
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
- - -
-
-
-
- } - -
- + +
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ } + +
+ - + } @code { @@ -469,6 +478,7 @@ else private string _cookiename; private string _cookieexpiration; private string _alwaysremember; + private string _logouteverywhere; private string _minimumlength; private string _uniquecharacters; @@ -529,7 +539,7 @@ else await LoadUsersAsync(true); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - _allowregistration = PageState.Site.AllowRegistration.ToString(); + _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @@ -538,6 +548,7 @@ else _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application"); _cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", ""); _alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false"); + _logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false"); _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6"); _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1"); @@ -656,6 +667,7 @@ else settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false); + settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 34884fb5..3b0bda17 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -507,4 +507,10 @@ Error Deleting User + + Logout Everywhere? + + + Do you want users to be logged out of every active session on any device, or only their current session? + \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 5f1f5d5d..5dd940f8 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -573,7 +573,7 @@ else { // post to the Logout page to complete the logout process - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) }; var interop = new Interop(jsRuntime); await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields); } diff --git a/Oqtane.Client/Themes/Controls/Theme/Login.razor b/Oqtane.Client/Themes/Controls/Theme/Login.razor index 69bd2922..f1bf1ebc 100644 --- a/Oqtane.Client/Themes/Controls/Theme/Login.razor +++ b/Oqtane.Client/Themes/Controls/Theme/Login.razor @@ -15,6 +15,7 @@
+
} diff --git a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs index 099be1d5..846cbcb7 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs +++ b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using Oqtane.Enums; -using Oqtane.Models; using Oqtane.Providers; using Oqtane.Security; using Oqtane.Services; @@ -26,6 +25,7 @@ namespace Oqtane.Themes.Controls protected string loginurl; protected string logouturl; protected string returnurl; + protected string everywhere; protected override void OnParametersSet() { @@ -57,6 +57,7 @@ namespace Oqtane.Themes.Controls // set logout url logouturl = Utilities.TenantUrl(PageState.Alias, "/pages/logout/"); + everywhere = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false"); // verify anonymous users can access current page if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsEffectiveAndNotExpired(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate)) @@ -98,7 +99,7 @@ namespace Oqtane.Themes.Controls else // this condition is only valid for legacy Login button inheriting from LoginBase { // post to the Logout page to complete the logout process - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl }; + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) }; var interop = new Interop(jsRuntime); await interop.SubmitForm(logouturl, fields); } From fcaf80cba62b79cb3222160e378499b40c57d4da Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 10 Mar 2025 10:56:11 -0400 Subject: [PATCH 55/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 241ab719..3d7db5b3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Try It Now! -Microsoft's Public Cloud (requires an Azure Tenant) +Microsoft's Public Cloud (requires an Azure account) [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fdev%2Fazuredeploy.json) A free ASP.NET hosting account. No hidden fees. No credit card required. From 0b1c7e06ca01e4c52f081a8151416be04987c0da Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 10 Mar 2025 10:56:41 -0400 Subject: [PATCH 56/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d7db5b3..22858202 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 source code from the Dev/Master branch:** -- Install **[.NET 9.0.1 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.2 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. From b9c59137a86f7c213787a2313b95408d3ac90cf1 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 11 Mar 2025 22:58:06 +0800 Subject: [PATCH 57/60] Fix #5156: update the bind event to oninput. --- Oqtane.Client/Modules/Admin/Login/Index.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f3f22c9c..f10c0203 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -29,12 +29,12 @@ else {
- +
- +
From 8d4b30140eee99feeeb6672c88dd5984cc18d5f0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 11 Mar 2025 11:48:43 -0400 Subject: [PATCH 58/60] rename Cache service to OutputCache --- .../OqtaneServiceCollectionExtensions.cs | 2 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 4 +- .../Modules/Admin/SystemInfo/Index.razor | 4 +- .../Resources/Modules/Admin/Site/Index.resx | 2 +- .../Modules/Admin/SystemInfo/Index.resx | 5 +- Oqtane.Client/Services/CacheService.cs | 23 -------- ...CacheService.cs => IOutputCacheService.cs} | 4 +- Oqtane.Client/Services/OutputCacheService.cs | 23 ++++++++ Oqtane.Server/Controllers/CacheController.cs | 31 ---------- .../Controllers/EndpointController.cs | 56 +++++++++++++++++++ .../Controllers/OutputCacheController.cs | 30 ++++++++++ .../OqtaneServiceCollectionExtensions.cs | 2 +- ...{CacheService.cs => OutputCacheService.cs} | 8 +-- 13 files changed, 127 insertions(+), 67 deletions(-) delete mode 100644 Oqtane.Client/Services/CacheService.cs rename Oqtane.Client/Services/Interfaces/{ICacheService.cs => IOutputCacheService.cs} (72%) create mode 100644 Oqtane.Client/Services/OutputCacheService.cs delete mode 100644 Oqtane.Server/Controllers/CacheController.cs create mode 100644 Oqtane.Server/Controllers/EndpointController.cs create mode 100644 Oqtane.Server/Controllers/OutputCacheController.cs rename Oqtane.Server/Services/{CacheService.cs => OutputCacheService.cs} (74%) diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 76389888..c5590d51 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -53,7 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index d33bfb85..5333cd3b 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -14,7 +14,7 @@ @inject IStringLocalizer Localizer @inject INotificationService NotificationService @inject IStringLocalizer SharedLocalizer -@inject ICacheService CacheService +@inject IOutputCacheService CacheService @if (_initialized) { @@ -936,7 +936,7 @@ } private async Task EvictSitemapOutputCache() { - await CacheService.EvictOutputCacheByTag(Constants.SitemapOutputCacheTag); + await CacheService.EvictByTag(Constants.SitemapOutputCacheTag); AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success); } } diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index 8195b656..e7329d7a 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -153,8 +153,10 @@

  - @Localizer["Swagger"]  +

+ @Localizer["Swagger"]  + @Localizer["Endpoints"]
diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index f0d23f03..0127c7cc 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -445,6 +445,6 @@ Clear Cache - SiteMap Output Cache Evicted + Site Map Cache Cleared \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index 6a2cafee..19bdf93a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Access Swagger UI + Swagger UI Framework Version @@ -306,4 +306,7 @@ Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled. + + API Endpoints + \ No newline at end of file diff --git a/Oqtane.Client/Services/CacheService.cs b/Oqtane.Client/Services/CacheService.cs deleted file mode 100644 index 4856b1e9..00000000 --- a/Oqtane.Client/Services/CacheService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -using Oqtane.Documentation; -using Oqtane.Shared; - -namespace Oqtane.Services -{ - /// - [PrivateApi("Don't show in the documentation, as everything should use the Interface")] - public class CacheService : ServiceBase, ICacheService - { - public CacheService(HttpClient http, SiteState siteState) : base(http, siteState) { } - - private string ApiUrl => CreateApiUrl("Cache"); - - public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) - { - await DeleteAsync($"{ApiUrl}/outputCache/evictByTag/{tag}"); - } - } -} diff --git a/Oqtane.Client/Services/Interfaces/ICacheService.cs b/Oqtane.Client/Services/Interfaces/IOutputCacheService.cs similarity index 72% rename from Oqtane.Client/Services/Interfaces/ICacheService.cs rename to Oqtane.Client/Services/Interfaces/IOutputCacheService.cs index 910cde25..08826146 100644 --- a/Oqtane.Client/Services/Interfaces/ICacheService.cs +++ b/Oqtane.Client/Services/Interfaces/IOutputCacheService.cs @@ -6,13 +6,13 @@ namespace Oqtane.Services /// /// Service to manage cache /// - public interface ICacheService + public interface IOutputCacheService { /// /// Evicts the output cache for a specific tag /// /// /// - Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default); + Task EvictByTag(string tag); } } diff --git a/Oqtane.Client/Services/OutputCacheService.cs b/Oqtane.Client/Services/OutputCacheService.cs new file mode 100644 index 00000000..40b43752 --- /dev/null +++ b/Oqtane.Client/Services/OutputCacheService.cs @@ -0,0 +1,23 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + /// + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class OutputCacheService : ServiceBase, IOutputCacheService + { + public OutputCacheService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string ApiUrl => CreateApiUrl("OutputCache"); + + public async Task EvictByTag(string tag) + { + await DeleteAsync($"{ApiUrl}/{tag}"); + } + } +} diff --git a/Oqtane.Server/Controllers/CacheController.cs b/Oqtane.Server/Controllers/CacheController.cs deleted file mode 100644 index e2d0ed08..00000000 --- a/Oqtane.Server/Controllers/CacheController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -using Oqtane.Models; -using Oqtane.Services; -using Oqtane.Shared; - -namespace Oqtane.Controllers -{ - [Route(ControllerRoutes.ApiRoute)] - public class CacheController : Controller - { - private readonly ICacheService _cacheService; - - public CacheController(ICacheService cacheService) - { - _cacheService = cacheService; - } - - // DELETE api//outputCache/evictByTag/{tag} - [HttpDelete("outputCache/evictByTag/{tag}")] - [Authorize(Roles = RoleNames.Admin)] - public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) - { - await _cacheService.EvictOutputCacheByTag(tag, cancellationToken); - } - } -} diff --git a/Oqtane.Server/Controllers/EndpointController.cs b/Oqtane.Server/Controllers/EndpointController.cs new file mode 100644 index 00000000..5fea4619 --- /dev/null +++ b/Oqtane.Server/Controllers/EndpointController.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Oqtane.Models; +using Oqtane.Shared; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class EndpointController : Controller + { + private readonly IEnumerable _endpointSources; + + public EndpointController(IEnumerable endpointSources) + { + _endpointSources = endpointSources; + } + + // GET api/ + [HttpGet] + [Authorize(Roles = RoleNames.Host)] + public ActionResult Get() + { + var endpoints = _endpointSources + .SelectMany(item => item.Endpoints) + .OfType(); + + var output = endpoints.Select( + item => + { + var controller = item.Metadata + .OfType() + .FirstOrDefault(); + var action = controller != null + ? $"{controller.ControllerName}.{controller.ActionName}" + : null; + var controllerMethod = controller != null + ? $"{controller.ControllerTypeInfo.FullName}:{controller.MethodInfo.Name}" + : null; + return new + { + Method = item.Metadata.OfType().FirstOrDefault()?.HttpMethods?[0], + Route = $"/{item.RoutePattern.RawText.TrimStart('/')}", + Action = action, + ControllerMethod = controllerMethod + }; + } + ); + + return Json(output); + } + } +} diff --git a/Oqtane.Server/Controllers/OutputCacheController.cs b/Oqtane.Server/Controllers/OutputCacheController.cs new file mode 100644 index 00000000..59a8ab09 --- /dev/null +++ b/Oqtane.Server/Controllers/OutputCacheController.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class OutputCacheController : Controller + { + private readonly IOutputCacheService _cacheService; + + public OutputCacheController(IOutputCacheService cacheService) + { + _cacheService = cacheService; + } + + // DELETE api//{tag} + [HttpDelete("{tag}")] + [Authorize(Roles = RoleNames.Admin)] + public async Task EvictByTag(string tag) + { + await _cacheService.EvictByTag(tag); + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index bf34816d..9b4b09e9 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -117,7 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection // services services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); // repositories services.AddTransient(); diff --git a/Oqtane.Server/Services/CacheService.cs b/Oqtane.Server/Services/OutputCacheService.cs similarity index 74% rename from Oqtane.Server/Services/CacheService.cs rename to Oqtane.Server/Services/OutputCacheService.cs index ed54d547..520c93b1 100644 --- a/Oqtane.Server/Services/CacheService.cs +++ b/Oqtane.Server/Services/OutputCacheService.cs @@ -12,24 +12,24 @@ using Oqtane.Shared; namespace Oqtane.Services { [PrivateApi("Don't show in the documentation, as everything should use the Interface")] - public class ServerCacheService : ICacheService + public class ServerOutputCacheService : IOutputCacheService { private readonly IOutputCacheStore _outputCacheStore; private readonly ILogManager _logger; private readonly IHttpContextAccessor _accessor; - public ServerCacheService(IOutputCacheStore outputCacheStore, ILogManager logger, IHttpContextAccessor accessor) + public ServerOutputCacheService(IOutputCacheStore outputCacheStore, ILogManager logger, IHttpContextAccessor accessor) { _outputCacheStore = outputCacheStore; _logger = logger; _accessor = accessor; } - public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) + public async Task EvictByTag(string tag) { if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin)) { - await _outputCacheStore.EvictByTagAsync(tag, cancellationToken); + await _outputCacheStore.EvictByTagAsync(tag, default); _logger.Log(LogLevel.Information, this, LogFunction.Other, "Evicted Output Cache for Tag {Tag}", tag); } else From 262d6a1529b25326b983d3bb2ed7e280a6090d2f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 11 Mar 2025 13:11:19 -0400 Subject: [PATCH 59/60] sort endpoints by route --- Oqtane.Client/Modules/Admin/SystemInfo/Index.razor | 2 +- Oqtane.Server/Controllers/EndpointController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index e7329d7a..f4d347e9 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -155,7 +155,7 @@  

- @Localizer["Swagger"]  + @Localizer["Swagger"]  @Localizer["Endpoints"] diff --git a/Oqtane.Server/Controllers/EndpointController.cs b/Oqtane.Server/Controllers/EndpointController.cs index 5fea4619..5cfff917 100644 --- a/Oqtane.Server/Controllers/EndpointController.cs +++ b/Oqtane.Server/Controllers/EndpointController.cs @@ -48,7 +48,7 @@ namespace Oqtane.Controllers ControllerMethod = controllerMethod }; } - ); + ).OrderBy(item => item.Route); return Json(output); } From 45610f8dd785c06196431a71e3160c06032c8905 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 11 Mar 2025 14:35:59 -0400 Subject: [PATCH 60/60] upgrade to .NET 9.0.3 --- Oqtane.Client/Oqtane.Client.csproj | 8 ++++---- .../Oqtane.Database.MySQL.csproj | 2 +- .../Oqtane.Database.PostgreSQL.csproj | 2 +- .../Oqtane.Database.SqlServer.csproj | 2 +- .../Oqtane.Database.Sqlite.csproj | 2 +- Oqtane.Maui/Oqtane.Maui.csproj | 16 ++++++++-------- Oqtane.Server/Oqtane.Server.csproj | 18 +++++++++--------- .../[Owner].Module.[Module].Client.csproj | 10 +++++----- .../[Owner].Module.[Module].Server.csproj | 8 ++++---- .../Client/[Owner].Theme.[Theme].Client.csproj | 6 +++--- Oqtane.Shared/Oqtane.Shared.csproj | 8 ++++---- 11 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 70a2988f..d8fca67b 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index 9d96167d..75b41b27 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -34,7 +34,7 @@ - + diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 1fabf6d4..c6b19d06 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -34,7 +34,7 @@ - + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index d7495ed4..35333476 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index ca3e298d..fe2e5713 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 0ca38927..ef62ef19 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -67,14 +67,14 @@ - - - - - - - - + + + + + + + + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index ea704ea7..a7205ca8 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -34,20 +34,20 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 8f410e40..5c41d71c 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 00763292..41a11759 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index 34c34f31..facf7f31 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@ - - - + + + diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index a8affac1..a3a0fe81 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -19,11 +19,11 @@ - - - + + + - +