diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 457af666..c5590d51 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -52,6 +52,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); 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/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 {
- +
- +
diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index 4693891e..803f2437 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -63,24 +63,7 @@
- @if (!string.IsNullOrEmpty(_packagename)) - { -
- - @if (string.IsNullOrEmpty(_packageurl)) - { - - } - else - { - @SharedLocalizer["Download"] - } -
- } - else - { - - } +
@@ -244,7 +227,6 @@ private string _moduledefinitionname = ""; private string _version; private string _packagename = ""; - private string _packageurl = ""; private string _owner = ""; private string _url = ""; private string _contact = ""; @@ -445,27 +427,5 @@ } } - private async Task ValidatePackage() - { - try - { - var package = await PackageService.GetPackageAsync(_packagename, _version, true); - if (package == null || string.IsNullOrEmpty(package.PackageUrl)) - { - AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning); - } - else - { - _packageurl = package.PackageUrl; - AddModuleMessage(Localizer["Message.Download"], MessageType.Info); - } - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version); - AddModuleMessage(Localizer["Error.Validate"], MessageType.Error); - } - } private string Browse(Page page) => string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url; } diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 6deef0eb..a4bc6500 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -16,7 +16,7 @@
- +
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) @@ -101,13 +101,13 @@
- +
- +
@@ -147,7 +147,7 @@
- +
@@ -186,13 +186,13 @@
- +
- +
diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 436eb45a..6d27b990 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -205,13 +205,13 @@
- +
- +
diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index c23f9732..5333cd3b 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -14,6 +14,7 @@ @inject IStringLocalizer Localizer @inject INotificationService NotificationService @inject IStringLocalizer SharedLocalizer +@inject IOutputCacheService CacheService @if (_initialized) { @@ -50,11 +51,12 @@
- +
@Localizer["Browse"] +
@@ -72,20 +74,8 @@
-
+
-
- -
- -
-
-
- -
- -
-
@@ -126,6 +116,32 @@
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
@@ -415,6 +431,7 @@ private string _themetype = ""; private string _containertype = ""; private string _admincontainertype = ""; + private string _cookieconsent = ""; private Dictionary _textEditors = new Dictionary(); private string _textEditor = ""; @@ -505,6 +522,7 @@ _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; + _cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty); // functionality var textEditors = ServiceProvider.GetServices(); @@ -717,6 +735,9 @@ settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); + //cookie consent + settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent); + // functionality settings = SettingService.SetSetting(settings, "TextEditor", _textEditor); @@ -913,4 +934,9 @@ _aliasname = ""; StateHasChanged(); } + + private async Task EvictSitemapOutputCache() { + 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..f4d347e9 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/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor index 92dcf3a8..ed324ebf 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -45,24 +45,7 @@
- @if (!string.IsNullOrEmpty(_packagename)) - { -
- - @if (string.IsNullOrEmpty(_packageurl)) - { - - } - else - { - @SharedLocalizer["Download"] - } -
- } - else - { - - } +
@@ -116,7 +99,6 @@ private string _name; private string _version; private string _packagename = ""; - private string _packageurl = ""; private string _owner = ""; private string _url = ""; private string _contact = ""; @@ -185,27 +167,4 @@ AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); } } - - private async Task ValidatePackage() - { - try - { - var package = await PackageService.GetPackageAsync(_packagename, _version, true); - if (package == null || string.IsNullOrEmpty(package.PackageUrl)) - { - AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning); - } - else - { - _packageurl = package.PackageUrl; - AddModuleMessage(Localizer["Message.Download"], MessageType.Info); - } - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version); - AddModuleMessage(Localizer["Error.Validate"], MessageType.Error); - } - } } diff --git a/Oqtane.Client/Modules/Admin/Themes/Index.razor b/Oqtane.Client/Modules/Admin/Themes/Index.razor index db655868..b6b25345 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Index.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Index.razor @@ -4,6 +4,7 @@ @inject NavigationManager NavigationManager @inject IThemeService ThemeService @inject IPackageService PackageService +@inject ISiteService SiteService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -19,6 +20,7 @@ else
+       @SharedLocalizer["Name"] @@ -32,10 +34,11 @@ else @if (context.AssemblyName != Constants.ClientId) - { + { - } + } + @Localizer["Assign"] @context.Name @context.Version 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/Modules/HtmlText/Index.razor b/Oqtane.Client/Modules/HtmlText/Index.razor index 6679cf83..c3fa8b39 100644 --- a/Oqtane.Client/Modules/HtmlText/Index.razor +++ b/Oqtane.Client/Modules/HtmlText/Index.razor @@ -2,6 +2,7 @@ @namespace Oqtane.Modules.HtmlText @inherits ModuleBase @inject IHtmlTextService HtmlTextService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @if (PageState.EditMode) @@ -36,6 +37,10 @@ { content = htmltext.Content; content = Utilities.FormatContent(content, PageState.Alias, "render"); + if (bool.Parse(SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"))) + { + content = ReplaceTokens(content); + } } else { diff --git a/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs b/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs index ecd90c8b..59a473c4 100644 --- a/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs +++ b/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs @@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText Version = "1.0.1", ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server", ReleaseVersions = "1.0.0,1.0.1", - SettingsType = string.Empty, + SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client", Resources = new List() { new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" } diff --git a/Oqtane.Client/Modules/HtmlText/Settings.razor b/Oqtane.Client/Modules/HtmlText/Settings.razor new file mode 100644 index 00000000..783e1180 --- /dev/null +++ b/Oqtane.Client/Modules/HtmlText/Settings.razor @@ -0,0 +1,55 @@ +@namespace Oqtane.Modules.HtmlText +@inherits ModuleBase +@inject ISettingService SettingService +@implements Oqtane.Interfaces.ISettingsControl +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+
+ +
+ +
+
+
+
+ +@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/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 4439f011..d8fca67b 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net9.0 Exe Debug;Release - 6.1.0 + 6.1.1 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -22,10 +22,10 @@ - - - - + + + + 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 diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index 7bb6ae59..186fdd65 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -228,18 +228,6 @@ View License - - Error Validating Package - - - Package Version Has Been Verified. Please Select The Download Button To Obtain The Package. - - - This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation - - - Validate - Browse diff --git a/Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx index 52f19942..9916c575 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - You Must Provide Some Search Criteria + Please Enter Some Search Criteria No Content Matches The Criteria Provided diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 0551fb8f..0127c7cc 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -349,7 +349,7 @@ Relay Configured? - The site map url for this site which can be submitted to search engines for indexing + The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared. Site Map: @@ -426,4 +426,25 @@ System + + Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent. + + + Cookie Consent: + + + Opt-In (GDPR) + + + Opt-Out (CCPA) + + + Theme + + + Clear Cache + + + 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/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index 58a04abe..c18f9761 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -180,16 +180,4 @@ View License - - Error Validating Package - - - Package Version Has Been Verified. Please Select The Download Button To Obtain The Package. - - - This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation - - - Validate - \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx index e6b11297..7ff1b618 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx @@ -146,7 +146,7 @@ Install Theme - + View @@ -156,4 +156,7 @@ Enabled? + + Assign + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 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/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.Client/Resources/Themes/Controls/CookieConsent.resx b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx new file mode 100644 index 00000000..1e5fc1e2 --- /dev/null +++ b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Confirm + + + 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 + + \ 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..fce3db5d 100644 --- a/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx +++ b/Oqtane.Client/Resources/Themes/OqtaneTheme/ThemeSettings.resx @@ -1,4 +1,4 @@ - + Exe - 6.1.0 + 6.1.1 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -30,7 +30,7 @@ com.oqtane.maui - 6.1.0 + 6.1.1 1 @@ -67,14 +67,14 @@ - - - - - - - - + + + + + + + + diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 719eb63e..191d9823 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/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.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index ae243057..3ace78bf 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 6.1.0 + 6.1.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 085cad61..0ab95f9b 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 6.1.0 + 6.1.1 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v6.1.0/Oqtane.Framework.6.1.0.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/download/v6.1.1/Oqtane.Framework.6.1.1.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 2f5380a9..9331b895 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 6.1.0 + 6.1.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 92b94496..937ac8bf 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 6.1.0 + 6.1.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index c0e5e5db..f55170f2 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 6.1.0 + 6.1.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 7336d3b4..9f6f5f82 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 80e44c4d..70b816f3 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Upgrade.zip" -Force diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 712f7145..868755aa 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -31,6 +31,8 @@ @inject IUrlMappingRepository UrlMappingRepository @inject IVisitorRepository VisitorRepository @inject IJwtManager JwtManager +@inject ICookieConsentService CookieConsentService +@inject ISettingService SettingService @if (_initialized) { @@ -107,6 +109,7 @@ private string _styleSheets = ""; private string _scripts = ""; private string _message = ""; + private bool _allowCookies; private PageState _pageState; // CascadingParameter is required to access HttpContext @@ -140,6 +143,9 @@ _prerender = site.Prerender; _fingerprint = site.Fingerprint; + var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty); + _allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout"); + var modules = new List(); Route route = new Route(url, alias.Path); @@ -170,7 +176,7 @@ modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId); } - if (site.VisitorTracking) + if (site.VisitorTracking && _allowCookies) { TrackVisitor(site.SiteId); } @@ -245,7 +251,8 @@ ReturnUrl = "", IsInternalNavigation = false, RenderId = Guid.NewGuid(), - Refresh = true + Refresh = true, + AllowCookies = _allowCookies }; } else diff --git a/Oqtane.Server/Controllers/CookieConsentController.cs b/Oqtane.Server/Controllers/CookieConsentController.cs new file mode 100644 index 00000000..7cb0a577 --- /dev/null +++ b/Oqtane.Server/Controllers/CookieConsentController.cs @@ -0,0 +1,52 @@ +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("IsActioned")] + public async Task IsActioned() + { + return await _cookieConsentService.IsActionedAsync(); + } + + [HttpGet("CanTrack")] + public async Task CanTrack(string optout) + { + return await _cookieConsentService.CanTrackAsync(bool.Parse(optout)); + } + + [HttpGet("CreateActionedCookie")] + public async Task CreateActionedCookie() + { + return await _cookieConsentService.CreateActionedCookieAsync(); + } + + [HttpGet("CreateConsentCookie")] + public async Task CreateConsentCookie() + { + return await _cookieConsentService.CreateConsentCookieAsync(); + } + + [HttpGet("WithdrawConsentCookie")] + public async Task WithdrawConsentCookie() + { + return await _cookieConsentService.WithdrawConsentCookieAsync(); + } + } +} diff --git a/Oqtane.Server/Controllers/EndpointController.cs b/Oqtane.Server/Controllers/EndpointController.cs new file mode 100644 index 00000000..5cfff917 --- /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 + }; + } + ).OrderBy(item => item.Route); + + return Json(output); + } + } +} 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 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/Controllers/VisitorController.cs b/Oqtane.Server/Controllers/VisitorController.cs index 224aaa5f..66ba447c 100644 --- a/Oqtane.Server/Controllers/VisitorController.cs +++ b/Oqtane.Server/Controllers/VisitorController.cs @@ -34,7 +34,7 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - return _visitors.GetVisitors(SiteId, DateTime.ParseExact(fromdate, "yyyy-MM-dd", CultureInfo.InvariantCulture)); + return _visitors.GetVisitors(SiteId, DateTime.ParseExact(fromdate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)); } else { diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index 3e466a20..3e31ac60 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -34,6 +34,14 @@ namespace Oqtane.Extensions options.SetDefaultCulture(defaultCulture) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); + + foreach(var culture in options.SupportedCultures) + { + if (culture.TextInfo.IsRightToLeft) + { + RightToLeftCulture.ResolveFormat(culture); + } + } }); return app; @@ -47,6 +55,5 @@ namespace Oqtane.Extensions public static IApplicationBuilder UseExceptionMiddleWare(this IApplicationBuilder builder) => builder.UseMiddleware(); - } } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index b04f2061..9b4b09e9 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(); @@ -116,6 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection // services services.AddTransient(); services.AddTransient(); + services.AddTransient(); // repositories services.AddTransient(); 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(); 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; 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/RightToLeftCultureCalendar.cs b/Oqtane.Server/Infrastructure/Localization/RightToLeftCultureCalendar.cs new file mode 100644 index 00000000..56b420d9 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Localization/RightToLeftCultureCalendar.cs @@ -0,0 +1,77 @@ +using System; + +namespace Oqtane.Infrastructure +{ + public class RightToLeftCultureCalendar : 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/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index 90b351ef..7727e9aa 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -1,14 +1,22 @@ using System.Collections.Generic; +using Microsoft.Extensions.Localization; using Oqtane.Documentation; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Shared; -namespace Oqtane.SiteTemplates +namespace Oqtane.Infrastructure.SiteTemplates { [PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")] public class AdminSiteTemplate : ISiteTemplate { + private readonly IStringLocalizer _localizer; + + public AdminSiteTemplate(IStringLocalizer localizer) + { + _localizer = localizer; + } + public string Name { get { return "Admin Site Template"; } @@ -169,6 +177,66 @@ namespace Oqtane.SiteTemplates } }); + pageTemplates.Add(new PageTemplate + { + Name = "Privacy", + Parent = "", + Path = "privacy", + Icon = Icons.Eye, + 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 = "Privacy Policy", 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) + }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, + Content = _localizer["Privacy"] + } + } + }); + + pageTemplates.Add(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 of Use", 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) + }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, + Content = _localizer["Terms"] + } + } + }); + pageTemplates.Add(new PageTemplate { Name = "Not Found", diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 09b1f30a..00498def 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -7,7 +7,7 @@ using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; -namespace Oqtane.SiteTemplates +namespace Oqtane.Infrastructure.SiteTemplates { [PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")] public class DefaultSiteTemplate : ISiteTemplate diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs index 265fca52..7cc0eb07 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs @@ -4,7 +4,7 @@ using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Shared; -namespace Oqtane.SiteTemplates +namespace Oqtane.Infrastructure.SiteTemplates { [PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")] public class EmptySiteTemplate : ISiteTemplate diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 210961c4..409ab59c 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; @@ -75,6 +77,9 @@ namespace Oqtane.Infrastructure case "6.1.0": Upgrade_6_1_0(tenant, scope); break; + case "6.1.1": + Upgrade_6_1_1(tenant, scope); + break; } } } @@ -457,6 +462,75 @@ namespace Oqtane.Infrastructure RemoveAssemblies(tenant, assemblies, "6.1.0"); } + private void Upgrade_6_1_1(Tenant tenant, IServiceScope scope) + { + var localizer = scope.ServiceProvider.GetRequiredService>(); + + var pageTemplates = new List + { + new PageTemplate + { + Name = "Privacy", + Parent = "", + Path = "privacy", + Icon = Icons.Eye, + 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 = "Privacy Policy", 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) + }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, + 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 of Use", 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) + }, + Settings = new List { + new Setting { SettingName = "DynamicTokens", SettingValue = "true" } + }, + Content = localizer["Terms"] + } + } + } + }; + + AddPagesToSites(scope, tenant, pageTemplates); + } + private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) { var tenants = scope.ServiceProvider.GetRequiredService(); diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 5c1852df..a7205ca8 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.0 + 6.1.1 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -34,21 +34,21 @@
- - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index 5af6737a..b241388e 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -50,7 +50,6 @@ namespace Oqtane.Pages { if (string.IsNullOrWhiteSpace(path)) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return BrokenFile(); } diff --git a/Oqtane.Server/Pages/Sitemap.cshtml.cs b/Oqtane.Server/Pages/Sitemap.cshtml.cs index 96501294..c564fb3c 100644 --- a/Oqtane.Server/Pages/Sitemap.cshtml.cs +++ b/Oqtane.Server/Pages/Sitemap.cshtml.cs @@ -7,6 +7,7 @@ using System.Xml; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.OutputCaching; using Microsoft.Extensions.DependencyInjection; using Oqtane.Enums; using Oqtane.Infrastructure; @@ -19,6 +20,7 @@ using Oqtane.Shared; namespace Oqtane.Pages { [AllowAnonymous] + [OutputCache(Duration = 300, Tags = [Constants.SitemapOutputCacheTag])] public class SitemapModel : PageModel { private readonly IServiceProvider _serviceProvider; 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(); }); 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); 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/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; diff --git a/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx b/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx new file mode 100644 index 00000000..c13f4563 --- /dev/null +++ b/Oqtane.Server/Resources/Infrastructure/SiteTemplates/AdminSiteTemplate.resx @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 [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. 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>[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> + +<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 [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>[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 [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. [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> + +<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 [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 [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> + +<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 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 a court of law and you consent to exclusive jurisdiction and venue of such courts.</p> + +<h2>Indemnification</h2> + +<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>[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.Server/Services/CookieConsentService.cs b/Oqtane.Server/Services/CookieConsentService.cs new file mode 100644 index 00000000..dd2ce21d --- /dev/null +++ b/Oqtane.Server/Services/CookieConsentService.cs @@ -0,0 +1,120 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Options; +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class ServerCookieConsentService : ICookieConsentService + { + private readonly IHttpContextAccessor _accessor; + private readonly CookiePolicyOptions _cookiePolicyOptions; + + public ServerCookieConsentService(IHttpContextAccessor accessor, IOptions cookiePolicyOptions) + { + _accessor = accessor; + _cookiePolicyOptions = cookiePolicyOptions.Value; + } + + public Task IsActionedAsync() + { + var actioned = false; + if (_accessor.HttpContext != null) + { + var cookieValue = GetCookieValue("actioned"); + actioned = cookieValue == Constants.CookieConsentActionCookieValue; + } + return Task.FromResult(actioned); + } + + public Task CanTrackAsync(bool optOut) + { + var canTrack = true; + if (_accessor.HttpContext != null) + { + var cookieValue = GetCookieValue("consent"); + var saved = cookieValue == Constants.CookieConsentCookieValue; + if (optOut) + { + canTrack = string.IsNullOrEmpty(cookieValue) || !saved; + } + else + { + canTrack = cookieValue == Constants.CookieConsentCookieValue; + } + } + + return Task.FromResult(canTrack); + } + + public Task CreateActionedCookieAsync() + { + var cookieString = CreateCookieString(false, string.Empty); + return Task.FromResult(cookieString); + } + + public Task CreateConsentCookieAsync() + { + var cookieString = CreateCookieString(true, Constants.CookieConsentCookieValue); + return Task.FromResult(cookieString); + } + + public Task WithdrawConsentCookieAsync() + { + var cookieString = CreateCookieString(true, string.Empty); + return Task.FromResult(cookieString); + } + + private string GetCookieValue(string type) + { + var cookieValue = string.Empty; + if (_accessor.HttpContext != null) + { + var value = _accessor.HttpContext.Request.Cookies[Constants.CookieConsentCookieName]; + var index = type == "actioned" ? 1 : 0; + cookieValue = !string.IsNullOrEmpty(value) && value.Contains("|") ? value.Split('|')[index] : string.Empty; + } + + return cookieValue; + } + + private string CreateCookieString(bool saved, string savedValue) + { + var cookieString = string.Empty; + if (_accessor.HttpContext != null) + { + var savedCookie = saved ? savedValue : GetCookieValue("consent"); + var actionedCookie = Constants.CookieConsentActionCookieValue; + var cookieValue = $"{savedCookie}|{actionedCookie}"; + var options = _cookiePolicyOptions.ConsentCookie.Build(_accessor.HttpContext); + + if (!_accessor.HttpContext.Response.HasStarted) + { + _accessor.HttpContext.Response.Cookies.Append( + Constants.CookieConsentCookieName, + cookieValue, + new CookieOptions() + { + Expires = options.Expires, + IsEssential = true, + SameSite = options.SameSite, + Secure = options.Secure + } + ); + } + + //get the cookie string from response header + cookieString = options.CreateCookieHeader(Constants.CookieConsentCookieName, Uri.EscapeDataString(cookieValue)).ToString(); + } + + return cookieString; + } + } +} diff --git a/Oqtane.Server/Services/OutputCacheService.cs b/Oqtane.Server/Services/OutputCacheService.cs new file mode 100644 index 00000000..520c93b1 --- /dev/null +++ b/Oqtane.Server/Services/OutputCacheService.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching; + +using Oqtane.Documentation; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class ServerOutputCacheService : IOutputCacheService + { + private readonly IOutputCacheStore _outputCacheStore; + private readonly ILogManager _logger; + private readonly IHttpContextAccessor _accessor; + + public ServerOutputCacheService(IOutputCacheStore outputCacheStore, ILogManager logger, IHttpContextAccessor accessor) + { + _outputCacheStore = outputCacheStore; + _logger = logger; + _accessor = accessor; + } + + public async Task EvictByTag(string tag) + { + if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin)) + { + await _outputCacheStore.EvictByTagAsync(tag, default); + _logger.Log(LogLevel.Information, this, LogFunction.Other, "Evicted Output Cache for Tag {Tag}", tag); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Output Cache Eviction for {Tag}", tag); + } + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 8ab42dd8..319d1471 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -142,6 +142,8 @@ namespace Oqtane }); }); + services.AddOutputCache(); + services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); @@ -222,6 +224,7 @@ namespace Oqtane app.UseJwtAuthorization(); app.UseRouting(); app.UseCors(); + app.UseOutputCache(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); 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 dce1e10e..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 57bbbf94..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/Oqtane.Themes.BlazorTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css index 99020c83..911362c5 100644 --- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css @@ -30,7 +30,6 @@ .breadcrumbs { background-color: #e6e6e6; - border-bottom: 1px solid #d6d5d5; } .top-row { @@ -120,12 +119,7 @@ .app-logo .navbar-brand { color: white; } - -@media (max-width: 767.98px) { - .main .top-row { - display: none; - } - +@media (max-width: 991.98px) { .app-search { border-radius: 6px; } @@ -141,7 +135,7 @@ .app-search:active, .app-search:hover { display: block; - position: fixed; + position: absolute; color: #fff; top: 0; min-height: 60px; @@ -149,6 +143,7 @@ left: 0; z-index: 999; border-radius: 0; + background-color: #e6e6e6; } .app-search:active .app-form-inline, .app-search:hover .app-form-inline { @@ -169,6 +164,11 @@ padding-bottom: 6px; } } +@media (max-width: 767.98px) { + .main .top-row { + display: none; + } +} @media (min-width: 768px) { app { @@ -257,6 +257,7 @@ width: 100%; left: 0; z-index: 4; + border-bottom: 1px solid #d6d5d5; } .sidebar { diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css index 6bb1aacb..c40b7140 100644 --- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css @@ -99,7 +99,49 @@ div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu color: #ffffff; z-index: 1000; } +@media (max-width: 991.98px) { + .app-search { + border-radius: 6px; + } + .app-search input{ + display: none !important; + } + .app-search input + button { + position: initial; + padding-top: 7px; + padding-bottom: 7px; + } + + .app-search:active, .app-search:hover { + display: block; + position: fixed; + top: 0; + min-height: 96px; + width: 100%; + left: 0; + z-index: 999; + border-radius: 0; + } + + .app-search:active .app-form-inline, .app-search:hover .app-form-inline { + margin: 10px auto; + position: relative; + display: block; + max-width: 80%; + } + + .app-search:active .app-form-inline input, .app-search:hover .app-form-inline input { + width: 100%; + display: block !important; + } + .app-search:active .app-form-inline input + button, .app-search:hover .app-form-inline input + button { + position: absolute; + color: rgb(42, 159, 214); + padding-top: 6px; + padding-bottom: 6px; + } +} @media (max-width: 767.98px) { .app-menu { @@ -130,45 +172,7 @@ div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu position: relative; top: 60px; } - .app-search { - border-radius: 6px; - } - .app-search input{ - display: none !important; - } - - .app-search input + button { - position: initial; - padding-top: 7px; - padding-bottom: 7px; - } - - .app-search:active, .app-search:hover { - display: block; - position: fixed; - top: 0; + .app-search:active, .app-search:hover{ min-height: 60px; - width: 100%; - left: 0; - z-index: 999; - border-radius: 0; - } - - .app-search:active .app-form-inline, .app-search:hover .app-form-inline { - margin: 10px auto; - position: relative; - display: block; - max-width: 80%; - } - - .app-search:active .app-form-inline input, .app-search:hover .app-form-inline input { - width: 100%; - display: block !important; - } - .app-search:active .app-form-inline input + button, .app-search:hover .app-form-inline input + button { - position: absolute; - color: rgb(42, 159, 214); - padding-top: 6px; - padding-bottom: 6px; } } 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 ec578462..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.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index 85350ca6..0b13a837 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -271,4 +271,14 @@ app { .app-logo .navbar-brand { padding: 5px 20px 5px 20px; +} + +/* cookie consent */ +.gdpr-consent-bar .btn-show{ + bottom: -3px; + left: 5px; +} +.gdpr-consent-bar .btn-hide{ + top: 0; + right: 5px; } \ No newline at end of file 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.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 6766e74d..528a8551 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,18 @@ 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.nonce = script.nonce; // must be referenced explicitly newScript.innerHTML = script.innerHTML; - newScript.removeAttribute('data-reload'); // dynamically injected scripts cannot be async or deferred newScript.async = false; @@ -73,10 +75,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 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/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)] diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 91f13025..a3a0fe81 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.0 + 6.1.1 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -19,11 +19,11 @@ - - - + + + - + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 447da2fa..0519de73 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "6.1.0"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0"; + public static readonly string Version = "6.1.1"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; @@ -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" }; @@ -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 = @@ -91,6 +91,11 @@ 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 CookieConsentCookieName = "Oqtane.CookieConsent"; + public const string CookieConsentCookieValue = "yes"; + public const string CookieConsentActionCookieValue = "yes"; + + public const string SitemapOutputCacheTag = "Sitemap"; // Obsolete constants const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames"; diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 58cd963e..4343cf9b 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net9.0 Exe - 6.1.0 + 6.1.1 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/README.md b/README.md index de03e405..22858202 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,21 @@ 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) +# Try It Now! + +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. +[![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:** -- Install **[.NET 9.0.0 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**. @@ -85,10 +91,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 diff --git a/SECURITY.md b/SECURITY.md index 0f88a666..52366db0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,9 @@ # Security Policy +## Security Bulletins + +All published security bulletins are available in the Oqtane [Security Center](https://www.oqtane.net/security). The Security Center allows you to select a specific version of the Oqtane Framework and view the associated security bulletins. + ## Reporting a Vulnerability We make every effort to ensure rapid and thorough analysis of reported issues and, where appropriate, provide workarounds and updated application releases to fix them. If you identify a potential security vulnerability please report it via support@oqtane.org. @@ -7,7 +11,7 @@ We make every effort to ensure rapid and thorough analysis of reported issues an All submitted information is viewed only by members of the Oqtane Security Team, and will not be discussed outside the Team without the permission of the person/company who reported the issue. Each confirmed issue is assigned a severity level (critical, moderate, or low) corresponding to its potential impact on an Oqtane installation. * **Critical** means the issue can be exploited by a remote attacker to gain access to data or functionality. All critical issue security bulletins include a recommended workaround or fix that should be applied as soon as possible. -* **Moderate** means the issue can compromise data or functionality on a portal/website only if some other condition is met (e.g. a particular module or a user within a particular role is required). Moderate issue security bulletins typically include recommended actions to resolve the issue. +* **Moderate** means the issue can compromise data or functionality on a site only if some other condition is met (e.g. a particular module or a user within a particular role is required). Moderate issue security bulletins typically include recommended actions to resolve the issue. * **Low** means the issue is very difficult to exploit or has a limited potential impact. Once an issue has been resolved via a public release of Oqtane, the release notes on GitHub are updated to reflect that security bulletins exist for the release. We strongly suggest using the "Watch" option on GitHub for "Releases" at a minimum to receive notifications of updated Oqtane releases. 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" + } } } ]