diff --git a/.gitignore b/.gitignore index 2d6b767b..bd2d7057 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ _ReSharper.Caches Oqtane.Server/appsettings.json Oqtane.Server/Data -/Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml +Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml Oqtane.Server/Content Oqtane.Server/Packages Oqtane.Server/wwwroot/Content diff --git a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor index 98729f76..69f3d9cb 100644 --- a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor +++ b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor @@ -13,7 +13,7 @@ { string url = NavigateUrl(p.Path);

- +

@((MarkupString)SharedLocalizer[p.Name].ToString().Replace(" ", "
"))

diff --git a/Oqtane.Client/Modules/Admin/Files/Add.razor b/Oqtane.Client/Modules/Admin/Files/Add.razor index d8655e1a..b5abe13c 100644 --- a/Oqtane.Client/Modules/Admin/Files/Add.razor +++ b/Oqtane.Client/Modules/Admin/Files/Add.razor @@ -4,6 +4,7 @@ @inject NavigationManager NavigationManager @inject IFileService FileService @inject IFolderService FolderService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -80,6 +81,7 @@ { validated = true; var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) { if (_url == string.Empty || _folderId == -1) @@ -93,7 +95,7 @@ _name = _url.Substring(_url.LastIndexOf("/", StringComparison.Ordinal) + 1); } - if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", ""))) + if (!PageState.Site.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", ""))) { AddModuleMessage(Localizer["Message.Download.InvalidExtension"], MessageType.Warning); return; diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index 6a9246e2..41926187 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -45,7 +45,7 @@
- +
diff --git a/Oqtane.Client/Modules/Admin/Languages/Add.razor b/Oqtane.Client/Modules/Admin/Languages/Add.razor index d8a52f33..23af68e5 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Add.razor @@ -131,8 +131,6 @@ else var interop = new Interop(JSRuntime); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); - - NavigationManager.NavigateTo(NavigationManager.Uri, true); } } diff --git a/Oqtane.Client/Modules/Admin/Languages/Edit.razor b/Oqtane.Client/Modules/Admin/Languages/Edit.razor new file mode 100644 index 00000000..5929edd9 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Languages/Edit.razor @@ -0,0 +1,110 @@ +@namespace Oqtane.Modules.Admin.Languages +@inherits ModuleBase +@using System.Globalization +@using Microsoft.AspNetCore.Localization +@inject NavigationManager NavigationManager +@inject ILocalizationService LocalizationService +@inject ILanguageService LanguageService +@inject IPackageService PackageService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +@if (_code == null) +{ +

@SharedLocalizer["Loading"]

+} +else +{ + + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + @SharedLocalizer["Cancel"] +
+
+
+} + +@code { + private ElementReference form; + private bool validated = false; + private int _languageId = -1; + private string _code = string.Empty; + private string _cultureName = string.Empty; + private string _default = "False"; + private List _languages; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + protected override async Task OnInitializedAsync() + { + _languageId = Int32.Parse(PageState.QueryString["id"]); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId); + Language language = _languages.Where(x => x.LanguageId == _languageId).FirstOrDefault(); + if (language != null) + { + _code = language.Code; + _cultureName = language.Name; + _default = language.IsDefault.ToString(); + if (language.SiteId == null) + { + language.SiteId = PageState.Site.SiteId; + } + }; + } + + private async Task SaveLanguage() + { + Language language = _languages.Where(x => x.LanguageId == _languageId).FirstOrDefault(); + if (language != null) + { + language.IsDefault = Boolean.Parse(_default); + try + { + await LanguageService.EditLanguageAsync(language); + + if (language.IsDefault) + { + await SetCultureAsync(language.Code); + } + + await logger.LogInformation("Language Edited {Language}", language); + + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Editing Language {Language} {Error}", language, ex.Message); + AddModuleMessage(Localizer["Error.Language.Edit"], MessageType.Error); + } + }; + } + + private async Task SetCultureAsync(string culture) + { + if (culture != CultureInfo.CurrentUICulture.Name) + { + var interop = new Interop(JSRuntime); + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); + } + } + +} diff --git a/Oqtane.Client/Modules/Admin/Languages/Index.razor b/Oqtane.Client/Modules/Admin/Languages/Index.razor index 18c98317..4b79388b 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Index.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Index.razor @@ -16,6 +16,7 @@ else
+     @SharedLocalizer["Name"] @Localizer["Code"] @@ -27,6 +28,12 @@ else }
+ + @if (!context.IsDefault) + { + + } + @context.Name @context.Code diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 73b22c94..b152a5d1 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -38,10 +38,10 @@
@if (!_alwaysremember) { -
- - -
+
+ + +
}
@@ -99,6 +99,7 @@ { _allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false; _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); + _alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false")); _togglepassword = SharedLocalizer["ShowPassword"]; @@ -155,10 +156,6 @@ AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info); } } - if (PageState.Site.Settings.TryGetValue("LoginOptions:AlwaysRemember", out string alwaysRememberStr)) - { - _alwaysremember = Convert.ToBoolean(alwaysRememberStr); - } } catch (Exception ex) { @@ -194,13 +191,7 @@ if (!twofactor) { - bool alwaysRemember = false; - if (PageState.Site.Settings.TryGetValue("LoginOptions:AlwaysRemember", out string alwaysRememberStr)) - { - alwaysRemember = Convert.ToBoolean(alwaysRememberStr); - } - bool remember = alwaysRemember || _remember; - _remember = remember; + _remember = _alwaysremember || _remember; user = await UserService.LoginUserAsync(user, hybrid, _remember); } else diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index 79f2a7b9..cf2f7cbc 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -81,7 +81,7 @@ else
- +
@@ -97,7 +97,7 @@ else private string _rows = "10"; private int _page = 1; private List _logs; - private string _retention = ""; + private int _retention = 30; public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}"; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -126,7 +126,7 @@ else await GetLogs(); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - _retention = SettingService.GetSetting(settings, "LogRetention", "30"); + _retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30")); } catch (Exception ex) { @@ -218,7 +218,7 @@ else try { var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - settings = SettingService.SetSetting(settings, "LogRetention", _retention, true); + settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index 23ed0ccc..9c1f4633 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -8,6 +8,8 @@ @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer +@inject IPageModuleService PageModuleService +@inject IModuleService ModuleService @if (_initialized) { @@ -131,10 +133,22 @@ @SharedLocalizer["Cancel"] + + +
+   + @SharedLocalizer["Name"] +
+ + + @(string.IsNullOrEmpty(context.Title) ? @context.Name : @context.Title ) + +
+
@if (_languages != null && _languages.Count > 0) { - +
@SharedLocalizer["Name"] @Localizer["Code"] @@ -240,6 +254,7 @@ private DateTime _createdon; private string _modifiedby; private DateTime _modifiedon; + private List _pagesWithModules; #pragma warning disable 649 private PermissionGrid _permissionGrid; @@ -291,6 +306,18 @@ _languages = _languages.OrderBy(item => item.Name).ToList(); } + // Group modules by PageId + // Get distinct PageIds where modules are present + var distinctPageIds = PageState.Modules + .Where(md => md.ModuleDefinition.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false) + .Select(md => md.PageId) + .Distinct(); + + // Filter and retrieve the corresponding pages + _pagesWithModules = PageState.Pages + .Where(pg => distinctPageIds.Contains(pg.PageId) && pg.IsDeleted == false) + .ToList(); + _initialized = true; } } @@ -439,5 +466,5 @@ 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 c55d4ec0..6e95fdf3 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -306,13 +306,15 @@ private void ThemeChanged(ChangeEventArgs e) { _themetype = (string)e.Value; + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); + _containertype = _containers.First().TypeName; + ThemeSettings(); + StateHasChanged(); + + // if theme chosen is different than default site theme, display warning message to user if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName) { AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning); - _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); - _containertype = _containers.First().TypeName; - ThemeSettings(); - StateHasChanged(); } } diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index d6043096..222d0119 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -447,13 +447,15 @@ private void ThemeChanged(ChangeEventArgs e) { _themetype = (string)e.Value; + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); + _containertype = _containers.First().TypeName; + ThemeSettings(); + StateHasChanged(); + + // if theme chosen is different than default site theme, display warning message to user if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName) { AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning); - _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); - _containertype = _containers.First().TypeName; - ThemeSettings(); - StateHasChanged(); } } diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 2cf45dc9..b7c80851 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -1,4 +1,5 @@ @namespace Oqtane.Modules.Admin.Register +@using System.Net @inherits ModuleBase @inject NavigationManager NavigationManager @inject IUserService UserService @@ -88,9 +89,9 @@ else } protected override void OnParametersSet() - { - _togglepassword = SharedLocalizer["ShowPassword"]; - } + { + _togglepassword = SharedLocalizer["ShowPassword"]; + } private async Task Register() { @@ -120,7 +121,14 @@ else if (user != null) { await logger.LogInformation("User Created {Username} {Email}", _username, _email); - AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info); + if (PageState.QueryString.ContainsKey("returnurl")) + { + NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"])); + } + else // legacy behavior + { + AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info); + } } else { diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index c56b538f..c55eb2ba 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -74,7 +74,7 @@
- +
@@ -119,6 +119,22 @@
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
@@ -207,7 +223,7 @@
- +
@@ -295,12 +311,11 @@
- +
@@ -313,6 +328,15 @@
+
+ +
+ +
+
@@ -378,7 +402,9 @@ private string _smtpsender = string.Empty; private string _smtprelay = "False"; private string _smtpenabled = "True"; - private string _retention = string.Empty; + private string _ImageFiles = string.Empty; + private string _UploadableFiles = string.Empty; + private int _retention = 30; private string _pwaisenabled; private int _pwaappiconfileid = -1; private FileManager _pwaappiconfilemanager; @@ -390,6 +416,7 @@ private string _defaultalias; private string _runtime = ""; private string _prerender = ""; + private string _hybridenabled = ""; private string _tenant = string.Empty; private string _database = string.Empty; private string _connectionstring = string.Empty; @@ -415,7 +442,7 @@ _homepageid = site.HomePageId.Value.ToString(); } _isdeleted = site.IsDeleted.ToString(); - _sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/pages/sitemap.xml"; + _sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml"; _siteguid = site.SiteGuid; _version = site.Version; @@ -461,7 +488,13 @@ _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False"); _smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True"); - _retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); + _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); + + // file extensions + _ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); + _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; + _UploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles); + _UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles; // aliases await GetAliases(); @@ -469,6 +502,7 @@ // hosting model _runtime = site.Runtime; _prerender = site.RenderMode.Replace(_runtime, ""); + _hybridenabled = site.HybridEnabled.ToString(); // database if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @@ -578,34 +612,35 @@ } // PWA - if (site.PwaIsEnabled.ToString() != _pwaisenabled) - { - site.PwaIsEnabled = Boolean.Parse(_pwaisenabled); - reload = true; // needs to be reloaded on server - } - int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId(); - if (pwaappiconfileid == -1) pwaappiconfileid = null; - if (site.PwaAppIconFileId != pwaappiconfileid) - { - site.PwaAppIconFileId = pwaappiconfileid; - reload = true; // needs to be reloaded on server - } - int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId(); - if (pwasplashiconfileid == -1) pwasplashiconfileid = null; - if (site.PwaSplashIconFileId != pwasplashiconfileid) - { - site.PwaSplashIconFileId = pwasplashiconfileid; - reload = true; // needs to be reloaded on server - } + if (site.PwaIsEnabled.ToString() != _pwaisenabled) + { + site.PwaIsEnabled = Boolean.Parse(_pwaisenabled); + reload = true; // needs to be reloaded on server + } + int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId(); + if (pwaappiconfileid == -1) pwaappiconfileid = null; + if (site.PwaAppIconFileId != pwaappiconfileid) + { + site.PwaAppIconFileId = pwaappiconfileid; + reload = true; // needs to be reloaded on server + } + int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId(); + if (pwasplashiconfileid == -1) pwasplashiconfileid = null; + if (site.PwaSplashIconFileId != pwasplashiconfileid) + { + site.PwaSplashIconFileId = pwasplashiconfileid; + reload = true; // needs to be reloaded on server + } // hosting model if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender) + if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender || site.HybridEnabled != bool.Parse(_hybridenabled)) { site.Runtime = _runtime; site.RenderMode = _runtime + _prerender; - reload = true; // needs to be reloaded on server + site.HybridEnabled = bool.Parse(_hybridenabled); + reload = true; // needs to be reloaded on serve } } @@ -622,7 +657,12 @@ settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true); settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true); settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true); - settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true); + settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); + + // file extensions + settings = SettingService.SetSetting(settings, "ImageFiles", (_ImageFiles != Constants.ImageFiles) ? _ImageFiles.Replace(" ", "") : "", false); + settings = SettingService.SetSetting(settings, "UploadableFiles", (_UploadableFiles != Constants.UploadableFiles) ? _UploadableFiles.Replace(" ", "") : "", false); + await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await logger.LogInformation("Site Settings Saved {Site}", site); diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index b940c151..2471ed63 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -76,7 +76,6 @@ else diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 58cc901e..be4c362f 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -1,4 +1,5 @@ @namespace Oqtane.Modules.Admin.UserProfile +@using System.Net @using System.Text.RegularExpressions; @inherits ModuleBase @inject NavigationManager NavigationManager @@ -76,7 +77,7 @@
- +
@@ -310,7 +311,7 @@ private int folderid = -1; private int photofileid = -1; private File photo = null; - + private string _ImageFiles = string.Empty; private List profiles; private Dictionary settings; private string category = string.Empty; @@ -337,6 +338,11 @@ email = PageState.User.Email; displayname = PageState.User.DisplayName; + if (string.IsNullOrEmpty(email)) + { + AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning); + } + // get user folder var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); if (folder != null) @@ -354,6 +360,8 @@ photofileid = -1; photo = null; } + var sitesettings = await SettingService.GetSiteSettingsAsync(SiteState.Alias.SiteId); + _ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); @@ -427,8 +435,15 @@ await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await logger.LogInformation("User Profile Saved"); - AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); - StateHasChanged(); + if (PageState.QueryString.ContainsKey("returnurl")) + { + NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"])); + } + else // legacy behavior + { + AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); + StateHasChanged(); + } } else { diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 82862c12..ed050ac7 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -195,7 +195,7 @@ else @if (_providertype != "") {
- +
@@ -254,7 +254,25 @@ else
-
+ @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) + { +
+ +
+ +
+
+ } +
@@ -266,21 +284,6 @@ else
-
- -
- -
-
@@ -296,33 +299,51 @@ else
-
- +
+ +
+
+ + @if (_reviewclaims == "true") + { + @SharedLocalizer["Test"] + } +
+
+
+
+
-
- +
+ +
+ +
+
+
+
- @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) - { -
- -
- -
+
+ +
+
-
- -
- -
+
+
+ +
+
- } +
@@ -351,10 +372,10 @@ else
- +
- +
@@ -425,12 +446,15 @@ else private string _clientsecret; private string _clientsecrettype = "password"; private string _toggleclientsecret = string.Empty; + private string _authresponsetype; private string _scopes; private string _parameters; private string _pkce; - private string _authresponsetype; private string _redirecturl; + private string _reviewclaims; + private string _externalloginurl; private string _identifierclaimtype; + private string _nameclaimtype; private string _emailclaimtype; private string _roleclaimtype; private string _profileclaimtypes; @@ -486,12 +510,15 @@ else _clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", ""); _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _toggleclientsecret = SharedLocalizer["ShowPassword"]; + _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); - _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; + _reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false"); + _externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external"); _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub"); + _nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name"); _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email"); _roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", ""); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); @@ -578,19 +605,20 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); - if (!string.IsNullOrEmpty(_secret) && _secret.Length < 16) _secret = (_secret + "????????????????").Substring(0, 16); settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); diff --git a/Oqtane.Client/Modules/Admin/Visitors/Index.razor b/Oqtane.Client/Modules/Admin/Visitors/Index.razor index 59e5739c..61487cf3 100644 --- a/Oqtane.Client/Modules/Admin/Visitors/Index.razor +++ b/Oqtane.Client/Modules/Admin/Visitors/Index.razor @@ -78,7 +78,7 @@ else
- +
@@ -104,7 +104,7 @@ else private List _visitors; private string _tracking; private string _filter = ""; - private string _retention = ""; + private int _retention = 30; private string _correlation = "true"; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -129,7 +129,7 @@ else _tracking = PageState.Site.VisitorTracking.ToString(); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); _filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); - _retention = SettingService.GetSetting(settings, "VisitorRetention", "30"); + _retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30")); _correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true"); } @@ -180,7 +180,7 @@ else var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); - settings = SettingService.SetSetting(settings, "VisitorRetention", _retention, true); + settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 5162c1f0..1bbe4581 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -3,6 +3,7 @@ @inherits ModuleControlBase @inject IFolderService FolderService @inject IFileService FileService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -83,6 +84,14 @@ } } } + @if (!string.IsNullOrEmpty(_message)) + { +
+
+ +
+
+ }
@if (_image != string.Empty) @@ -92,14 +101,6 @@ } - @if (!string.IsNullOrEmpty(_message)) - { -
-
- -
-
- } } @@ -343,6 +344,7 @@ _message = string.Empty; var interop = new Interop(JSRuntime); var uploads = await interop.GetFiles(_fileinputid); + if (uploads.Length > 0) { string restricted = ""; @@ -350,7 +352,7 @@ { var filename = upload.Split(':')[0]; var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : ""; - if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower())) + if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower())) { restricted += (restricted == "" ? "" : ",") + extension; } diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index ca490d52..ad7980d1 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -1,79 +1,86 @@ +@using System.Text.RegularExpressions @namespace Oqtane.Modules.Controls @inherits ModuleControlBase +@inject ISettingService SettingService @inject IStringLocalizer Localizer
- - @if (_richfilemanager) - { - - -
- } -
- @if (AllowRawHtml) - { - @((MarkupString)"  ") - } - @if (AllowFileManagement) - { - - } - @if (_richfilemanager) - { - @((MarkupString)"  ") - - } -
-
-
-
- @if (ToolbarContent != null) - { - @ToolbarContent - } - else - { - - - - - - - - - - - - - - - - - - - } -
-
-
-
-
-
+ @if (AllowRichText) + { + + @if (_richfilemanager) + { + + +
+ } +
+ @if (AllowRawHtml) + { + + + @((MarkupString)"  ") + } + @if (AllowFileManagement) + { + + } + @if (_richfilemanager) + { + @((MarkupString)"  ") + + } +
+
+
+
+ @if (ToolbarContent != null) + { + @ToolbarContent + } + else + { + + + + + + + + + + + + + + + + + + + } +
+
+
+
+
+
+ } @if (AllowRawHtml) { @if (_rawfilemanager) { - +
} @@ -104,119 +111,130 @@
@code { - private ElementReference _editorElement; - private ElementReference _toolBar; - private bool _richfilemanager = false; - private FileManager _fileManager; - private string _richhtml = string.Empty; - private string _originalrichhtml = string.Empty; - private bool _rawfilemanager = false; - private string _rawhtml = string.Empty; - private string _originalrawhtml = string.Empty; - private string _message = string.Empty; + private ElementReference _editorElement; + private ElementReference _toolBar; + private bool _richfilemanager = false; + private FileManager _fileManager; + private string _richhtml = string.Empty; + private string _originalrichhtml = string.Empty; + private bool _rawfilemanager = false; + private string _rawhtml = string.Empty; + private string _originalrawhtml = string.Empty; + private string _message = string.Empty; - [Parameter] - public string Content { get; set; } + [Parameter] + public string Content { get; set; } - [Parameter] - public bool ReadOnly { get; set; } = false; + [Parameter] + public bool ReadOnly { get; set; } = false; - [Parameter] - public string Placeholder { get; set; } = "Enter Your Content..."; + [Parameter] + public string Placeholder { get; set; } = "Enter Your Content..."; - [Parameter] - public bool AllowFileManagement { get; set; } = true; + [Parameter] + public bool AllowFileManagement { get; set; } = true; - [Parameter] - public bool AllowRawHtml { get; set; } = true; - - // parameters only applicable to rich text editor - [Parameter] - public RenderFragment ToolbarContent { get; set; } + [Parameter] + public bool AllowRichText { get; set; } = true; - [Parameter] - public string Theme { get; set; } = "snow"; + [Parameter] + public bool AllowRawHtml { get; set; } = true; - [Parameter] - public string DebugLevel { get; set; } = "info"; + // parameters only applicable to rich text editor + [Parameter] + public RenderFragment ToolbarContent { get; set; } - public override List Resources => new List() + [Parameter] + public string Theme { get; set; } = "snow"; + + [Parameter] + public string DebugLevel { get; set; } = "info"; + + public override List Resources => new List() { new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js" }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js" } }; - protected override void OnParametersSet() - { - _richhtml = Content; - _rawhtml = Content; - _originalrawhtml = _rawhtml; // preserve for comparison later - } + protected override void OnParametersSet() + { + _richhtml = Content; + _rawhtml = Content; + _originalrawhtml = _rawhtml; // preserve for comparison later + } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); - var interop = new RichTextEditorInterop(JSRuntime); + var interop = new RichTextEditorInterop(JSRuntime); - if (firstRender) - { - await interop.CreateEditor( - _editorElement, - _toolBar, - ReadOnly, - Placeholder, - Theme, - DebugLevel); + if (firstRender) + { + await interop.CreateEditor( + _editorElement, + _toolBar, + ReadOnly, + Placeholder, + Theme, + DebugLevel); - await interop.LoadEditorContent(_editorElement, _richhtml); + await interop.LoadEditorContent(_editorElement, _richhtml); - // preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor) - _originalrichhtml = await interop.GetHtml(_editorElement); - } - } + if (AllowRichText) + { + // preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor) + _originalrichhtml = await interop.GetHtml(_editorElement); + } + } + } - public void CloseRichFileManager() - { - _richfilemanager = false; - _message = string.Empty; - StateHasChanged(); - } + public void CloseRichFileManager() + { + _richfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } - public void CloseRawFileManager() - { - _rawfilemanager = false; - _message = string.Empty; - StateHasChanged(); - } + public void CloseRawFileManager() + { + _rawfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } - public void RefreshRichText() - { - _richhtml = _rawhtml; - StateHasChanged(); - } + public void RefreshRichText() + { + _richhtml = _rawhtml; + StateHasChanged(); + } - public async Task RefreshRawHtml() - { - var interop = new RichTextEditorInterop(JSRuntime); - _rawhtml = await interop.GetHtml(_editorElement); - StateHasChanged(); - } + public async Task RefreshRawHtml() + { + var interop = new RichTextEditorInterop(JSRuntime); + _rawhtml = await interop.GetHtml(_editorElement); + StateHasChanged(); + } - public async Task GetHtml() - { - // evaluate raw html content as first priority - if (_rawhtml != _originalrawhtml) - { - return _rawhtml; - } - else - { - // return rich text content if it has changed - var interop = new RichTextEditorInterop(JSRuntime); - var richhtml = await interop.GetHtml(_editorElement); - if (richhtml != _originalrichhtml) + public async Task GetHtml() + { + // evaluate raw html content as first priority + if (_rawhtml != _originalrawhtml) + { + return _rawhtml; + } + else + { + var richhtml = ""; + if (AllowRichText) + { + // return rich text content if it has changed + var interop = new RichTextEditorInterop(JSRuntime); + richhtml = await interop.GetHtml(_editorElement); + } + // rich text value will only be blank if AllowRichText is disabled or the JS Interop method failed + if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml) && !string.IsNullOrEmpty(_originalrichhtml)) { return richhtml; } diff --git a/Oqtane.Client/Modules/Controls/Section.razor b/Oqtane.Client/Modules/Controls/Section.razor index 91f91724..951fa971 100644 --- a/Oqtane.Client/Modules/Controls/Section.razor +++ b/Oqtane.Client/Modules/Controls/Section.razor @@ -1,27 +1,30 @@ @namespace Oqtane.Modules.Controls @inherits LocalizableComponent -
-
- -
@_heading
-
+@if (IsVisible) +{ + -
- -   - +
+
-
-
-
-
-
- @if (ChildContent != null) - { - @ChildContent - } -
+
+ @if (ChildContent != null) + { + @ChildContent + } +
+} @code { private string _heading = string.Empty; @@ -40,6 +43,9 @@ [Parameter] public string Expanded { get; set; } // optional - will default to false if not provided + [Parameter] + public bool IsVisible { get; set; } = true; + protected override void OnParametersSet() { base.OnParametersSet(); // must be included to call method in LocalizableComponent diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 41812923..f5c0bef7 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -261,7 +261,12 @@ namespace Oqtane.Modules // UI methods public void AddModuleMessage(string message, MessageType type) { - ModuleInstance.AddModuleMessage(message, type); + AddModuleMessage(message, type, "top"); + } + + public void AddModuleMessage(string message, MessageType type, string position) + { + ModuleInstance.AddModuleMessage(message, type, position); } public void ClearModuleMessage() diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index eea64f14..880f5e1b 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net8.0 Exe Debug;Release - 5.0.0 + 5.0.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/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Client/Resources/Modules/Admin/Languages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Languages/Edit.resx new file mode 100644 index 00000000..68458745 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/Languages/Edit.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Error Updating Language + + + Name Of The Langauage + + + Indicates Whether Or Not This Language Is The Default For The Site + + + Name: + + + Default? + + + Translation Package Saved Successfully. You Must <a href={0}>Restart</a> To Complete The Installation. + + + Upload one or more translation packages. + + + Translation + + + Manage + + + Upload + + + Error Loading Language + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 67dbbfed..3467911a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -204,8 +204,8 @@ Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions. - - The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions. + + The External Login Provider Did Not Provide All Of The Required Information. Please Contact Your Administrator For Further Instructions. An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions. @@ -225,4 +225,7 @@ Your External Login Failed. Please Contact Your Administrator For Further Instructions. + + The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index 87cc9a12..7bb6ae59 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -240,4 +240,10 @@ Validate + + Browse + + + Pages + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx index 0ff2e74d..ce91ee3b 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx @@ -127,7 +127,7 @@ Error Downloading Module - Are You Sure You Wish To Delete The {0} Module? + Are You Sure You Wish To Uninstall The {0} Module? Error Loading Modules @@ -142,10 +142,10 @@ Install Module - Delete Module + Uninstall Module - Delete + Uninstall In Use? diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 0d9b6c65..8d8efaf8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -283,7 +283,7 @@ Prerender? - The Blazor runtime hosting model + The Blazor runtime hosting model for the site Runtime: @@ -402,4 +402,25 @@ Retention (Days): + + File Extensions + + + Enter a comma separated list of image file extensions + + + Image Extensions: + + + Enter a comma separated list of uploadable file extensions + + + Uploadable File Extensions: + + + Specifies if the site can be integrated with an external .NET MAUI hybrid application + + + Hybrid Enabled? + \ 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 e8e41589..e6b11297 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx @@ -124,7 +124,7 @@ Error Downloading Theme - Are You Sure You Wish To Delete The {0} Theme? + Are You Sure You Wish To Uninstall The {0} Theme? Error Loading Themes @@ -136,10 +136,10 @@ Error Deleting Theme - Delete Theme + Uninstall Theme - Delete + Uninstall Create Theme diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index 8bcffb8c..7e6b222d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -147,6 +147,9 @@ Current User Is Not Logged In + + You Must Provide An Email Address For Your User Account + Error Loading User Profile diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index e6d4792b..6baae9c1 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -247,7 +247,7 @@ Domain Filter: - The name of the email address claim provided by the identity provider + Optionally specify the type name of the email address claim provided by the identity provider. The typical value is 'email'. Email Claim: @@ -274,7 +274,7 @@ Use PKCE? - The external login provider name which will be displayed on the login page + Specify a friendly name for the external login provider which will be displayed on the Login page Provider Name: @@ -373,7 +373,7 @@ Last Login - The name of the unique user identifier claim provided by the identity provider + Specify the type name of the unique user identifier claim provided by the identity provider. The default value is 'sub'. Identifier Claim: @@ -385,13 +385,13 @@ Parameters: - Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site. + Optionally provide the type name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site. Role Claim: - Optionally provide a comma delimited list of user profile claims provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'. + Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'. User Profile Claims: @@ -409,31 +409,34 @@ Import Users - code + Authorization Code - code id_token + Authorization Code + ID Token - code id_token token + Authorization Code + ID Token + Access Token - code token + Authorization Code + Access Token - id_token + ID Token - id_token token + ID Token + Access Token - none + None - token + Access Token - - Authorization Response Type + + Authorization Response Type: + + + Specify the authorization response type. The default is Authorization Code which is considered to be the most secure option based on the latest OAuth specification. Do you want existing users to perform an additional email verification step to link their external login? If you disable this option, existing users will be linked automatically. @@ -453,4 +456,16 @@ Cookie Expiration Timespan: + + Review Claims? + + + This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled. + + + Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'. + + + Name Claim: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index b225811b..b00c7ce9 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -432,4 +432,10 @@ {0} Is Required + + Uninstall + + + Test + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/ILanguageService.cs b/Oqtane.Client/Services/Interfaces/ILanguageService.cs index a2da3a2f..bf7aa9a7 100644 --- a/Oqtane.Client/Services/Interfaces/ILanguageService.cs +++ b/Oqtane.Client/Services/Interfaces/ILanguageService.cs @@ -1,6 +1,6 @@ -using Oqtane.Models; using System.Collections.Generic; using System.Threading.Tasks; +using Oqtane.Models; namespace Oqtane.Services { @@ -39,6 +39,13 @@ namespace Oqtane.Services /// Task AddLanguageAsync(Language language); + /// + /// Edits the given language + /// + /// + /// + Task EditLanguageAsync(Language language); + /// /// Deletes the given language /// diff --git a/Oqtane.Client/Services/LanguageService.cs b/Oqtane.Client/Services/LanguageService.cs index f8b432c8..7e95e952 100644 --- a/Oqtane.Client/Services/LanguageService.cs +++ b/Oqtane.Client/Services/LanguageService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; @@ -10,7 +9,7 @@ namespace Oqtane.Services { [PrivateApi("Don't show in the documentation, as everything should use the Interface")] public class LanguageService : ServiceBase, ILanguageService - { + { public LanguageService(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("Language"); @@ -35,6 +34,11 @@ namespace Oqtane.Services return await PostJsonAsync(Apiurl, language); } + public async Task EditLanguageAsync(Language language) + { + await PutJsonAsync(Apiurl, language); + } + public async Task DeleteLanguageAsync(int languageId) { await DeleteAsync($"{Apiurl}/{languageId}"); diff --git a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs index b3de5229..f8f71ee9 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs +++ b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs @@ -26,8 +26,15 @@ namespace Oqtane.Themes.Controls var allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false; var allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path); - var returnurl = WebUtility.UrlEncode(route.PathAndQuery); + var returnurl = ""; + if (!PageState.QueryString.ContainsKey("returnurl")) + { + returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery); // remember current url + } + else + { + returnurl = PageState.QueryString["returnurl"]; // use existing value + } if (allowexternallogin && !allowsitelogin) { @@ -39,7 +46,6 @@ namespace Oqtane.Themes.Controls // local login NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + returnurl)); } - } protected async Task LogoutUser() diff --git a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor index 36a5e0c1..850e28c3 100644 --- a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor +++ b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor @@ -1,4 +1,5 @@ @namespace Oqtane.Themes.Controls +@using System.Net @inherits ThemeControlBase @inject IStringLocalizer Localizer @@ -26,14 +27,21 @@ [Parameter] public bool ShowRegister { get; set; } + private string _returnurl = ""; + + protected override void OnParametersSet() + { + _returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery); + } + private void RegisterUser() { - NavigationManager.NavigateTo(NavigateUrl("register")); + NavigationManager.NavigateTo(NavigateUrl("register", "returnurl=" + _returnurl)); } private void UpdateProfile() { - NavigationManager.NavigateTo(NavigateUrl("profile")); + NavigationManager.NavigateTo(NavigateUrl("profile", "returnurl=" + _returnurl)); } } diff --git a/Oqtane.Client/UI/ModuleInstance.razor b/Oqtane.Client/UI/ModuleInstance.razor index f05ef9be..78ae6d5c 100644 --- a/Oqtane.Client/UI/ModuleInstance.razor +++ b/Oqtane.Client/UI/ModuleInstance.razor @@ -5,7 +5,10 @@ @if (CurrentException is null) { - + if (_messagePosition == "top") + { + + } @if (ModuleType != null) { @@ -14,6 +17,10 @@
} } + if (_messagePosition == "bottom") + { + + } } else { @@ -24,49 +31,58 @@ else } @code { - private string _message; - private string _error; - private MessageType _messageType; - private bool _progressIndicator = false; + private string _message; + private string _error; + private MessageType _messageType; + private string _messagePosition; + private bool _progressIndicator = false; - private Type ModuleType { get; set; } - private IDictionary ModuleParameters { get; set; } + private Type ModuleType { get; set; } + private IDictionary ModuleParameters { get; set; } - [CascadingParameter] - protected PageState PageState { get; set; } + [CascadingParameter] + protected PageState PageState { get; set; } - [CascadingParameter] - private Module ModuleState { get; set; } + [CascadingParameter] + private Module ModuleState { get; set; } - private ModuleMessage ModuleMessage { get; set; } + private ModuleMessage ModuleMessage { get; set; } - protected override void OnParametersSet() - { - _message = ""; - if (!string.IsNullOrEmpty(ModuleState.ModuleType)) - { - ModuleType = Type.GetType(ModuleState.ModuleType); - if (ModuleType != null) - { - ModuleParameters = new Dictionary { { "ModuleInstance", this } }; - return; - } - // module does not exist with typename specified - _message = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0)); - _messageType = MessageType.Error; - } - else - { - _message = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName); - _messageType = MessageType.Error; - } - } + protected override void OnParametersSet() + { + _message = ""; + if (!string.IsNullOrEmpty(ModuleState.ModuleType)) + { + ModuleType = Type.GetType(ModuleState.ModuleType); + if (ModuleType != null) + { + ModuleParameters = new Dictionary { { "ModuleInstance", this } }; + return; + } + // module does not exist with typename specified + _message = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0)); + _messageType = MessageType.Error; + _messagePosition = "top"; + } + else + { + _message = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName); + _messageType = MessageType.Error; + _messagePosition = "top"; + } + } - public void AddModuleMessage(string message, MessageType type) - { - _message = message; - _messageType = type; - _progressIndicator = false; + public void AddModuleMessage(string message, MessageType type) + { + AddModuleMessage(message, type, "top"); + } + + public void AddModuleMessage(string message, MessageType type, string position) + { + _message = message; + _messageType = type; + _messagePosition = position; + _progressIndicator = false; StateHasChanged(); } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 8035841e..51877aa0 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -105,11 +105,18 @@ Route route = new Route(_absoluteUri, SiteState.Alias.Path); int moduleid = (int.TryParse(route.ModuleId, out moduleid)) ? moduleid : -1; var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction; + var querystring = Utilities.ParseQueryString(route.Query); var returnurl = ""; if (querystring.ContainsKey("returnurl")) { returnurl = WebUtility.UrlDecode(querystring["returnurl"]); + if (!returnurl.StartsWith("/")) + { + // urls which are not relative are vulnerable to open redirects or XSS + returnurl = ""; + querystring["returnurl"] = ""; + } } // reload the client application from the server if there is a forced reload @@ -155,7 +162,8 @@ if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId) { var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - if (authState.User.Identity.IsAuthenticated) + // verify user is authenticated for current site + if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey)) { user = await UserService.GetUserAsync(authState.User.Identity.Name, SiteState.Alias.SiteId); if (user != null) diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 920e362e..9432e2ac 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -1,4 +1,5 @@ @namespace Oqtane.UI +@using System.Net @inject IJSRuntime JSRuntime @inject NavigationManager NavigationManager @inject SiteState SiteState @@ -87,6 +88,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { + // force authenticated user to provide email address (email may be missing if using external login) + if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile") + { + NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery))); + return; + } + if (!firstRender) { if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains(" net8.0 - 5.0.0 + 5.0.1 Oqtane Shaun Walker .NET Foundation @@ -10,14 +10,10 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git true - true - $(MSBuildProjectName).nuspec - $(MSBuildProjectName).$(Version).nupkg - true @@ -27,6 +23,14 @@ bin + + + 1701;1702;EF1001;AD0001 + + + + 1701;1702;EF1001;AD0001 + @@ -37,7 +41,12 @@ - - + + + + + + + diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.nuspec b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.nuspec deleted file mode 100644 index 728dbb32..00000000 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.nuspec +++ /dev/null @@ -1,26 +0,0 @@ - - - - Oqtane.Database.MySQL - 5.0.0 - Shaun Walker - .NET Foundation - Oqtane MySQL Provider - MySQL database support for the Oqtane Framework - MySQL database support for the Oqtane Framework - .NET Foundation - false - MIT - https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 - icon.png - oqtane - - - - - - - - - \ No newline at end of file diff --git a/Oqtane.Database.MySQL/icon.png b/Oqtane.Database.MySQL/icon.png deleted file mode 100644 index 3c43cce4..00000000 Binary files a/Oqtane.Database.MySQL/icon.png and /dev/null differ diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 3264cca9..185f3e5f 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net8.0 - 5.0.0 + 5.0.1 Oqtane Shaun Walker .NET Foundation @@ -10,14 +10,10 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git true - true - $(MSBuildProjectName).nuspec - $(MSBuildProjectName).$(Version).nupkg - true @@ -27,18 +23,31 @@ bin + + + 1701;1702;EF1001;AD0001 + + + + 1701;1702;EF1001;AD0001 + - + - - + + + + + + + diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.nuspec b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.nuspec deleted file mode 100644 index b43d0d97..00000000 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.nuspec +++ /dev/null @@ -1,27 +0,0 @@ - - - - Oqtane.Database.PostgreSQL - 5.0.0 - Shaun Walker - .NET Foundation - Oqtane PostgreSQL Provider - PostgreSQL database support for the Oqtane Framework - PostgreSQL database support for the Oqtane Framework - .NET Foundation - false - MIT - https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 - icon.png - oqtane - - - - - - - - - - \ No newline at end of file diff --git a/Oqtane.Database.PostgreSQL/OqtaneHistoryRepository.cs b/Oqtane.Database.PostgreSQL/OqtaneHistoryRepository.cs index 3172946e..cbc96246 100644 --- a/Oqtane.Database.PostgreSQL/OqtaneHistoryRepository.cs +++ b/Oqtane.Database.PostgreSQL/OqtaneHistoryRepository.cs @@ -1,11 +1,9 @@ using System; -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; using Oqtane.Migrations.Framework; using Oqtane.Models; -using Oqtane.Shared; // ReSharper disable ClassNeverInstantiated.Global diff --git a/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs b/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs index eba5aa82..f8806564 100644 --- a/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs +++ b/Oqtane.Database.PostgreSQL/PostgreSQLDatabase.cs @@ -9,7 +9,6 @@ using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Npgsql; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Oqtane.Databases; -using Oqtane.Shared; namespace Oqtane.Database.PostgreSQL { diff --git a/Oqtane.Database.PostgreSQL/icon.png b/Oqtane.Database.PostgreSQL/icon.png deleted file mode 100644 index 3c43cce4..00000000 Binary files a/Oqtane.Database.PostgreSQL/icon.png and /dev/null differ diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index f8dd5c68..5b8d94b3 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net8.0 - 5.0.0 + 5.0.1 Oqtane Shaun Walker .NET Foundation @@ -10,14 +10,10 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git true - true - $(MSBuildProjectName).nuspec - $(MSBuildProjectName).$(Version).nupkg - true @@ -28,6 +24,14 @@ bin + + 1701;1702;EF1001;AD0001 + + + + 1701;1702;EF1001;AD0001 + + @@ -36,8 +40,12 @@ - - + + + + + + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.nuspec b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.nuspec deleted file mode 100644 index 9aed5043..00000000 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.nuspec +++ /dev/null @@ -1,25 +0,0 @@ - - - - Oqtane.Database.SqlServer - 5.0.0 - Shaun Walker - .NET Foundation - Oqtane SQL Server Provider - SQL Server database support for the Oqtane Framework - SQL Server database support for the Oqtane Framework - .NET Foundation - false - MIT - https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 - icon.png - oqtane - - - - - - - - \ No newline at end of file diff --git a/Oqtane.Database.SqlServer/OqtaneHistoryRepository.cs b/Oqtane.Database.SqlServer/OqtaneHistoryRepository.cs index 394aedb0..d3adacdf 100644 --- a/Oqtane.Database.SqlServer/OqtaneHistoryRepository.cs +++ b/Oqtane.Database.SqlServer/OqtaneHistoryRepository.cs @@ -1,13 +1,9 @@ using System; -using System.Text; -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.SqlServer.Migrations.Internal; -using Microsoft.EntityFrameworkCore.Storage; using Oqtane.Migrations.Framework; using Oqtane.Models; -using Oqtane.Shared; // ReSharper disable ClassNeverInstantiated.Global diff --git a/Oqtane.Database.SqlServer/SqlServerDatabase.cs b/Oqtane.Database.SqlServer/SqlServerDatabase.cs index 11384e17..a7d8f641 100644 --- a/Oqtane.Database.SqlServer/SqlServerDatabase.cs +++ b/Oqtane.Database.SqlServer/SqlServerDatabase.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Oqtane.Databases; -using Oqtane.Shared; namespace Oqtane.Database.SqlServer { diff --git a/Oqtane.Database.SqlServer/icon.png b/Oqtane.Database.SqlServer/icon.png deleted file mode 100644 index 3c43cce4..00000000 Binary files a/Oqtane.Database.SqlServer/icon.png and /dev/null differ diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 084eac98..6af2de3f 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net8.0 - 5.0.0 + 5.0.1 Oqtane Shaun Walker .NET Foundation @@ -10,14 +10,10 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git true - true - $(MSBuildProjectName).nuspec - $(MSBuildProjectName).$(Version).nupkg - true @@ -28,6 +24,14 @@ bin + + 1701;1702;EF1001;AD0001 + + + + 1701;1702;EF1001;AD0001 + + @@ -36,8 +40,12 @@ - - + + + + + + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.nuspec b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.nuspec deleted file mode 100644 index 8dee352a..00000000 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.nuspec +++ /dev/null @@ -1,25 +0,0 @@ - - - - Oqtane.Database.Sqlite - 5.0.0 - Shaun Walker - .NET Foundation - Oqtane SQLite Provider - SQLite database support for the Oqtane Framework - SQLite database support for the Oqtane Framework - .NET Foundation - false - MIT - https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 - icon.png - oqtane - - - - - - - - \ No newline at end of file diff --git a/Oqtane.Database.Sqlite/OqtaneHistoryRepository.cs b/Oqtane.Database.Sqlite/OqtaneHistoryRepository.cs index 4703714a..e1961358 100644 --- a/Oqtane.Database.Sqlite/OqtaneHistoryRepository.cs +++ b/Oqtane.Database.Sqlite/OqtaneHistoryRepository.cs @@ -1,11 +1,9 @@ using System; -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal; using Oqtane.Migrations.Framework; using Oqtane.Models; -using Oqtane.Shared; // ReSharper disable ClassNeverInstantiated.Global diff --git a/Oqtane.Database.Sqlite/SqliteDatabase.cs b/Oqtane.Database.Sqlite/SqliteDatabase.cs index df150c2c..39c6cfd3 100644 --- a/Oqtane.Database.Sqlite/SqliteDatabase.cs +++ b/Oqtane.Database.Sqlite/SqliteDatabase.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Oqtane.Databases; -using Oqtane.Shared; namespace Oqtane.Database.Sqlite { diff --git a/Oqtane.Database.Sqlite/icon.png b/Oqtane.Database.Sqlite/icon.png deleted file mode 100644 index 3c43cce4..00000000 Binary files a/Oqtane.Database.Sqlite/icon.png and /dev/null differ diff --git a/Oqtane.Databases.sln b/Oqtane.Databases.sln deleted file mode 100644 index 67deb124..00000000 --- a/Oqtane.Databases.sln +++ /dev/null @@ -1,56 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28822.285 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{77EECA8C-B58E-469E-B8C5-D543AFC9A654}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitignore = .gitignore - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.MySQL", "Oqtane.Database.MySQL\Oqtane.Database.MySQL.csproj", "{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.PostgreSQL", "Oqtane.Database.PostgreSQL\Oqtane.Database.PostgreSQL.csproj", "{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.Sqlite", "Oqtane.Database.Sqlite\Oqtane.Database.Sqlite.csproj", "{E4F50CA9-19A6-465A-9469-C033748AD95B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.SqlServer", "Oqtane.Database.SqlServer\Oqtane.Database.SqlServer.csproj", "{033DCA37-6354-4A3D-8250-4EC20740EE19}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Release|Any CPU.Build.0 = Release|Any CPU - {3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Release|Any CPU.Build.0 = Release|Any CPU - {E4F50CA9-19A6-465A-9469-C033748AD95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4F50CA9-19A6-465A-9469-C033748AD95B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4F50CA9-19A6-465A-9469-C033748AD95B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4F50CA9-19A6-465A-9469-C033748AD95B}.Release|Any CPU.Build.0 = Release|Any CPU - {033DCA37-6354-4A3D-8250-4EC20740EE19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {033DCA37-6354-4A3D-8250-4EC20740EE19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {033DCA37-6354-4A3D-8250-4EC20740EE19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {033DCA37-6354-4A3D-8250-4EC20740EE19}.Release|Any CPU.Build.0 = Release|Any CPU - {6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1FB11796-35DE-4AED-9A52-17733557FCC4} - EndGlobalSection -EndGlobal diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 831c4fa1..0b8d3657 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 5.0.0 + 5.0.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/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -31,7 +31,7 @@ 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 - 5.0.0 + 5.0.1 1 14.2 diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index c24d4c24..4c346c87 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 5.0.0 + 5.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index ac7b0882..3b0d65b4 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 5.0.0 + 5.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v5.0.0Oqtane.Framework.5.0.0.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/download/v5.0.1/Oqtane.Framework.5.0.1.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index fac5a6cb..65c3d5c4 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 5.0.0 + 5.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index b5b5d5f4..8c6be30a 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 5.0.0 + 5.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index dfd80f2a..aee8ee24 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 5.0.0 + 5.0.1 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index ebb62c0e..634cf373 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.0.Install.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.1.Install.zip" -Force \ No newline at end of file diff --git a/Oqtane.Package/release.cmd b/Oqtane.Package/release.cmd index d22478d5..0b11fe27 100644 --- a/Oqtane.Package/release.cmd +++ b/Oqtane.Package/release.cmd @@ -1,8 +1,6 @@ del "*.nupkg" del "*.zip" -dotnet clean -c Release ..\Oqtane.Databases.sln dotnet clean -c Release ..\Oqtane.sln -dotnet build -c Release ..\Oqtane.Databases.sln dotnet build -c Release ..\Oqtane.sln nuget.exe pack Oqtane.Client.nuspec nuget.exe pack Oqtane.Server.nuspec diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index f0c92eea..b9538e69 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.0.Upgrade.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.1.Upgrade.zip" -Force \ No newline at end of file diff --git a/Oqtane.Server/Controllers/AliasController.cs b/Oqtane.Server/Controllers/AliasController.cs index adaf3945..4d942cee 100644 --- a/Oqtane.Server/Controllers/AliasController.cs +++ b/Oqtane.Server/Controllers/AliasController.cs @@ -76,7 +76,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Alias Put(int id, [FromBody] Alias alias) { - if (ModelState.IsValid && _aliases.GetAlias(alias.AliasId, false) != null) + if (ModelState.IsValid && alias.AliasId == id && _aliases.GetAlias(alias.AliasId, false) != null) { alias = _aliases.UpdateAlias(alias); _syncManager.AddSyncEvent(alias.TenantId, EntityNames.Alias, alias.AliasId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 84a36886..65cd3fec 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -35,8 +35,8 @@ namespace Oqtane.Controllers private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - - public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + private readonly ISettingRepository _settingRepository; + public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) { _environment = environment; _files = files; @@ -45,6 +45,7 @@ namespace Oqtane.Controllers _syncManager = syncManager; _logger = logger; _alias = tenantManager.GetAlias(); + _settingRepository = settingRepository; } // GET: api/?folder=x @@ -207,7 +208,7 @@ namespace Oqtane.Controllers public Models.File Put(int id, [FromBody] Models.File file) { var File = _files.GetFile(file.FileId, false); - if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && File != null // ensure file exists + if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && file.FileId == id && File != null // ensure file exists && _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, File.FolderId, PermissionNames.Edit) // ensure user had edit rights to original folder && _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit)) // ensure user has edit rights to new folder { @@ -287,6 +288,8 @@ namespace Oqtane.Controllers folder = _folders.GetFolder(FolderId); } + var _UploadableFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue ?? Constants.UploadableFiles) ?? Constants.UploadableFiles; + if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList)) { string folderPath = _folders.GetFolderPath(folder); @@ -297,7 +300,7 @@ namespace Oqtane.Controllers name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1); } // check for allowable file extensions - if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", ""))) + if (!_UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", ""))) { _logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url); HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict; @@ -362,6 +365,10 @@ namespace Oqtane.Controllers return; } + // Get the UploadableFiles extensions + string uploadfilesSetting = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue; + string _UploadableFiles = uploadfilesSetting ?? Constants.UploadableFiles; + // ensure filename is valid string token = ".part_"; if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token)) @@ -371,7 +378,7 @@ namespace Oqtane.Controllers // check for allowable file extensions (ignore token) var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", ""); - if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower())) + if (!_UploadableFiles.Split(',').Contains(extension.ToLower())) { return; } @@ -604,9 +611,11 @@ namespace Oqtane.Controllers public IActionResult GetImage(int id, int width, int height, string mode, string position, string background, string rotate, string recreate) { var file = _files.GetFile(id); + + var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles; if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) { - if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) + if (_ImageFiles.Split(',').Contains(file.Extension.ToLower())) { var filepath = _files.GetFilePath(file); if (System.IO.File.Exists(filepath)) @@ -658,8 +667,15 @@ namespace Oqtane.Controllers } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + if (file != null) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + } } string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png"); @@ -763,6 +779,7 @@ namespace Oqtane.Controllers private Models.File CreateFile(string filename, int folderid, string filepath) { var file = _files.GetFile(folderid, filename); + var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles; int size = 0; var folder = _folders.GetFolder(folderid, false); @@ -789,7 +806,7 @@ namespace Oqtane.Controllers file.ImageHeight = 0; file.ImageWidth = 0; - if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) + if (_ImageFiles.Split(',').Contains(file.Extension.ToLower())) { try { diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 71190c2d..83fa8e5f 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -204,7 +204,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Registered)] public Folder Put(int id, [FromBody] Folder folder) { - if (ModelState.IsValid && folder.SiteId == _alias.SiteId && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit)) + if (ModelState.IsValid && folder.SiteId == _alias.SiteId && folder.FolderId == id && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit)) { if (folder.IsPathValid()) { diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 1e4c8716..b070fce6 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -30,11 +30,12 @@ namespace Oqtane.Controllers private readonly IMemoryCache _cache; private readonly IHttpContextAccessor _accessor; private readonly IAliasRepository _aliases; + private readonly ISiteRepository _sites; private readonly ILogger _filelogger; private readonly ITenantManager _tenantManager; private readonly IServerStateManager _serverState; - public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger filelogger, ITenantManager tenantManager, IServerStateManager serverState) + public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ISiteRepository sites, ILogger filelogger, ITenantManager tenantManager, IServerStateManager serverState) { _configManager = configManager; _installationManager = installationManager; @@ -43,6 +44,7 @@ namespace Oqtane.Controllers _cache = cache; _accessor = accessor; _aliases = aliases; + _sites = sites; _filelogger = filelogger; _tenantManager = tenantManager; _serverState = serverState; @@ -108,6 +110,70 @@ namespace Oqtane.Controllers return GetAssemblyList().Select(item => item.HashedName).ToList(); } + private List GetAssemblyList() + { + var alias = _tenantManager.GetAlias(); + + return _cache.GetOrCreate($"assemblieslist:{alias.SiteKey}", entry => + { + var assemblyList = new List(); + + var site = _sites.GetSite(alias.SiteId); + if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled)) + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // testmode setting is used for validating that the API is downloading the appropriate assemblies to the client + bool hashfilename = true; + if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true") + { + hashfilename = false; + } + + // get site assemblies which should be downloaded to client + var assemblies = _serverState.GetServerState(alias.SiteKey).Assemblies; + + // populate assembly list + foreach (var assembly in assemblies) + { + if (assembly != Constants.ClientId) + { + var filepath = Path.Combine(binFolder, assembly) + ".dll"; + if (System.IO.File.Exists(filepath)) + { + assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename)); + } + } + } + + // insert satellite assemblies at beginning of list + foreach (var culture in _localizationManager.GetInstalledCultures()) + { + if (culture != Constants.DefaultCulture) + { + var assembliesFolderPath = Path.Combine(binFolder, culture); + if (Directory.Exists(assembliesFolderPath)) + { + foreach (var assembly in assemblies) + { + var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll"; + if (System.IO.File.Exists(filepath)) + { + assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename)); + } + } + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); + } + } + } + } + return assemblyList; + }); + } + // GET api//load?list=x,y [HttpGet("load")] public IActionResult Load(string list = "*") @@ -115,126 +181,79 @@ namespace Oqtane.Controllers return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll"); } - private List GetAssemblyList() - { - var siteKey = _tenantManager.GetAlias().SiteKey; - - return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry => - { - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - var assemblyList = new List(); - - // testmode setting is used for validating that the API is downloading the appropriate assemblies to the client - bool hashfilename = true; - if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true") - { - hashfilename = false; - } - - // get site assemblies which should be downloaded to client - var assemblies = _serverState.GetServerState(siteKey).Assemblies; - - // populate assembly list - foreach (var assembly in assemblies) - { - if (assembly != Constants.ClientId) - { - var filepath = Path.Combine(binFolder, assembly) + ".dll"; - if (System.IO.File.Exists(filepath)) - { - assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename)); - } - } - } - - // insert satellite assemblies at beginning of list - foreach (var culture in _localizationManager.GetInstalledCultures()) - { - if (culture != Constants.DefaultCulture) - { - var assembliesFolderPath = Path.Combine(binFolder, culture); - if (Directory.Exists(assembliesFolderPath)) - { - foreach (var assembly in assemblies) - { - var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll"; - if (System.IO.File.Exists(filepath)) - { - assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename)); - } - } - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); - } - } - } - - return assemblyList; - }); - } - private byte[] GetAssemblies(string list) { - var siteKey = _tenantManager.GetAlias().SiteKey; + var alias = _tenantManager.GetAlias(); if (list == "*") { - return _cache.GetOrCreate($"assemblies:{siteKey}", entry => + return _cache.GetOrCreate($"assemblies:{alias.SiteKey}", entry => { - return GetZIP(list); + return GetZIP(list, alias); }); } else { - return GetZIP(list); + return GetZIP(list, alias); } } - private byte[] GetZIP(string list) + private byte[] GetZIP(string list, Alias alias) { - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - - // get list of assemblies which should be downloaded to client - List assemblies = GetAssemblyList(); - if (list != "*") + var site = _sites.GetSite(alias.SiteId); + if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled)) { - var filter = list.Split(',').ToList(); - assemblies.RemoveAll(item => !filter.Contains(item.HashedName)); - } + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - // create zip file containing assemblies and debug symbols - using (var memoryStream = new MemoryStream()) - { - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + // get list of assemblies which should be downloaded to client + List assemblies = GetAssemblyList(); + if (list != "*") { - foreach (var assembly in assemblies) + var filter = list.Split(',').ToList(); + assemblies.RemoveAll(item => !filter.Contains(item.HashedName)); + } + + // create zip file containing assemblies and debug symbols + using (var memoryStream = new MemoryStream()) + { + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) { - if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId) + foreach (var assembly in assemblies) { - if (System.IO.File.Exists(assembly.FilePath)) + if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId) { - using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(assembly.HashedName).Open()) + if (System.IO.File.Exists(assembly.FilePath)) { - filestream.CopyTo(entrystream); + using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(assembly.HashedName).Open()) + { + filestream.CopyTo(entrystream); + } } - } - var pdb = assembly.FilePath.Replace(".dll", ".pdb"); - if (System.IO.File.Exists(pdb)) - { - using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open()) + var pdb = assembly.FilePath.Replace(".dll", ".pdb"); + if (System.IO.File.Exists(pdb)) { - filestream.CopyTo(entrystream); + using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open()) + { + filestream.CopyTo(entrystream); + } } } } } - } - return memoryStream.ToArray(); + return memoryStream.ToArray(); + } + } + else + { + // return empty zip + using (var memoryStream = new MemoryStream()) + { + using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create)) {} + return memoryStream.ToArray(); + } } } diff --git a/Oqtane.Server/Controllers/JobController.cs b/Oqtane.Server/Controllers/JobController.cs index fb3bd99a..b2440de8 100644 --- a/Oqtane.Server/Controllers/JobController.cs +++ b/Oqtane.Server/Controllers/JobController.cs @@ -67,7 +67,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Job Put(int id, [FromBody] Job job) { - if (ModelState.IsValid && _jobs.GetJob(job.JobId, false) != null) + if (ModelState.IsValid && job.JobId == id && _jobs.GetJob(job.JobId, false) != null) { job = _jobs.UpdateJob(job); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", job); diff --git a/Oqtane.Server/Controllers/LanguageController.cs b/Oqtane.Server/Controllers/LanguageController.cs index e1ac9406..8ed5e4d4 100644 --- a/Oqtane.Server/Controllers/LanguageController.cs +++ b/Oqtane.Server/Controllers/LanguageController.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Reflection; using Microsoft.AspNetCore.Authorization; @@ -9,9 +12,6 @@ using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; -using System.Linq; -using System.Diagnostics; -using System.Globalization; namespace Oqtane.Controllers { @@ -102,6 +102,24 @@ namespace Oqtane.Controllers } } + [HttpPut] + [Authorize(Roles = RoleNames.Admin)] + public void Put([FromBody] Language language) + { + if (ModelState.IsValid && language.SiteId == _alias.SiteId) + { + _languages.UpdateLanguage(language); + _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Language, language.LanguageId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Language Updated {Language}", language); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Put Attempt {Language}", language); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + [HttpPost] [Authorize(Roles = RoleNames.Admin)] public Language Post([FromBody] Language language) diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 9422d019..f8172db1 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -154,7 +154,7 @@ namespace Oqtane.Controllers { var _module = _modules.GetModule(module.ModuleId, false); - if (ModelState.IsValid && module.SiteId == _alias.SiteId && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit)) + if (ModelState.IsValid && module.SiteId == _alias.SiteId && module.ModuleId == id && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit)) { module = _modules.UpdateModule(module); diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 94c4c085..4fbea7eb 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -167,7 +167,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Admin)] public void Put(int id, [FromBody] ModuleDefinition moduleDefinition) { - if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null) + if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && moduleDefinition.ModuleDefinitionId == id && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null) { _moduleDefinitions.UpdateModuleDefinition(moduleDefinition); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs index b5fc97d4..95fb8a07 100644 --- a/Oqtane.Server/Controllers/NotificationController.cs +++ b/Oqtane.Server/Controllers/NotificationController.cs @@ -161,6 +161,12 @@ namespace Oqtane.Controllers { if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId)) { + if (!User.IsInRole(RoleNames.Admin)) + { + // content must be HTML encoded for non-admins to prevent HTML injection + notification.Subject = WebUtility.HtmlEncode(notification.Subject); + notification.Body = WebUtility.HtmlEncode(notification.Body); + } notification = _notifications.AddNotification(notification); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Create); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {NotificationId}", notification.NotificationId); @@ -179,8 +185,14 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Registered)] public Notification Put(int id, [FromBody] Notification notification) { - if (ModelState.IsValid && notification.SiteId == _alias.SiteId && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId))) + if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId))) { + if (!User.IsInRole(RoleNames.Admin)) + { + // content must be HTML encoded for non-admins to prevent HTML injection + notification.Subject = WebUtility.HtmlEncode(notification.Subject); + notification.Body = WebUtility.HtmlEncode(notification.Body); + } notification = _notifications.UpdateNotification(notification); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId); diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 85085954..458586b6 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -269,7 +269,7 @@ namespace Oqtane.Controllers // get current page var currentPage = _pages.GetPage(page.PageId, false); - if (ModelState.IsValid && page.SiteId == _alias.SiteId && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit)) + if (ModelState.IsValid && page.SiteId == _alias.SiteId && page.PageId == id && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit)) { // get current page permissions var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList(); diff --git a/Oqtane.Server/Controllers/PageModuleController.cs b/Oqtane.Server/Controllers/PageModuleController.cs index 3b1db3cf..4f2c963d 100644 --- a/Oqtane.Server/Controllers/PageModuleController.cs +++ b/Oqtane.Server/Controllers/PageModuleController.cs @@ -109,7 +109,7 @@ namespace Oqtane.Controllers public PageModule Put(int id, [FromBody] PageModule pageModule) { var page = _pages.GetPage(pageModule.PageId); - if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit)) + if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && pageModule.PageModuleId == id && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit)) { pageModule = _pageModules.UpdatePageModule(pageModule); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.PageModule, pageModule.PageModuleId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/ProfileController.cs b/Oqtane.Server/Controllers/ProfileController.cs index 13e4e859..cd51b9ab 100644 --- a/Oqtane.Server/Controllers/ProfileController.cs +++ b/Oqtane.Server/Controllers/ProfileController.cs @@ -94,7 +94,7 @@ namespace Oqtane.Controllers [Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}")] public Profile Put(int id, [FromBody] Profile profile) { - if (ModelState.IsValid && profile.SiteId == _alias.SiteId && _profiles.GetProfile(profile.ProfileId, false) != null) + if (ModelState.IsValid && profile.SiteId == _alias.SiteId && profile.ProfileId == id && _profiles.GetProfile(profile.ProfileId, false) != null) { profile = _profiles.UpdateProfile(profile); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Profile, profile.ProfileId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/RoleController.cs b/Oqtane.Server/Controllers/RoleController.cs index 15a880ef..5b1c8948 100644 --- a/Oqtane.Server/Controllers/RoleController.cs +++ b/Oqtane.Server/Controllers/RoleController.cs @@ -98,7 +98,7 @@ namespace Oqtane.Controllers [Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}")] public Role Put(int id, [FromBody] Role role) { - if (ModelState.IsValid && role.SiteId == _alias.SiteId && _roles.GetRole(role.RoleId, false) != null) + if (ModelState.IsValid && role.SiteId == _alias.SiteId && role.RoleId == id && _roles.GetRole(role.RoleId, false) != null) { role = _roles.UpdateRole(role); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Role, role.RoleId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 9bb9fb8e..f1a2f6ff 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -128,7 +128,7 @@ namespace Oqtane.Controllers [HttpPut("{id}")] public Setting Put(int id, [FromBody] Setting setting) { - if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit)) + if (ModelState.IsValid && setting.SettingId == id && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit)) { setting = _settings.UpdateSetting(setting); AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/SiteController.cs b/Oqtane.Server/Controllers/SiteController.cs index 9283aa14..4d1b7184 100644 --- a/Oqtane.Server/Controllers/SiteController.cs +++ b/Oqtane.Server/Controllers/SiteController.cs @@ -86,6 +86,12 @@ namespace Oqtane.Controllers .Where(item => !item.IsPrivate || User.IsInRole(RoleNames.Admin)) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + // populate File Extensions + site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"]) + ? site.Settings["ImageFiles"] : Constants.ImageFiles; + site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"]) + ? site.Settings["UploadableFiles"] : Constants.UploadableFiles; + // pages List settings = _settings.GetSettings(EntityNames.Page).ToList(); site.Pages = new List(); @@ -192,7 +198,7 @@ namespace Oqtane.Controllers public Site Put(int id, [FromBody] Site site) { var current = _sites.GetSite(site.SiteId, false); - if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && current != null) + if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && site.SiteId == id && current != null) { site = _sites.UpdateSite(site); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, site.SiteId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 131c4b1d..40067fa3 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -71,7 +71,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Admin)] public void Put(int id, [FromBody] Theme theme) { - if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null) + if (ModelState.IsValid && theme.SiteId == _alias.SiteId && theme.ThemeId == id && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null) { _themes.UpdateTheme(theme); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/UrlMappingController.cs b/Oqtane.Server/Controllers/UrlMappingController.cs index a81e2c35..0b2f43e0 100644 --- a/Oqtane.Server/Controllers/UrlMappingController.cs +++ b/Oqtane.Server/Controllers/UrlMappingController.cs @@ -118,7 +118,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Admin)] public UrlMapping Put(int id, [FromBody] UrlMapping urlMapping) { - if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null) + if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && urlMapping.UrlMappingId == id && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null) { urlMapping = _urlMappings.UpdateUrlMapping(urlMapping); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UrlMapping, urlMapping.UrlMappingId, SyncEventActions.Update); diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index bb033977..a91d90e5 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -173,15 +173,16 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { + user.EmailConfirmed = User.IsInRole(RoleNames.Admin); user = await _userManager.UpdateUser(user); } else { user.Password = ""; // remove sensitive information - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; user = null; } diff --git a/Oqtane.Server/Controllers/UserRoleController.cs b/Oqtane.Server/Controllers/UserRoleController.cs index 271d7c05..8ef00098 100644 --- a/Oqtane.Server/Controllers/UserRoleController.cs +++ b/Oqtane.Server/Controllers/UserRoleController.cs @@ -149,7 +149,7 @@ namespace Oqtane.Controllers public UserRole Put(int id, [FromBody] UserRole userRole) { var role = _roles.GetRole(userRole.RoleId); - if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && _userRoles.GetUserRole(userRole.UserRoleId, false) != null) + if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && userRole.UserRoleId == id && _userRoles.GetUserRole(userRole.UserRoleId, false) != null) { userRole = _userRoles.UpdateUserRole(userRole); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Update); diff --git a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 00000000..9e664f0f --- /dev/null +++ b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Security.Claims; + +namespace Oqtane.Extensions +{ + public static class ClaimsPrincipalExtensions + { + public static string Username(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name)) + { + return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name).Value; + } + else + { + return ""; + } + } + + public static int UserId(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.NameIdentifier)) + { + return int.Parse(claimsPrincipal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value); + } + else + { + return -1; + } + } + + public static string Roles(this ClaimsPrincipal claimsPrincipal) + { + var roles = ""; + foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)) + { + roles += ((roles == "") ? "" : ";") + claim.Value; + } + return roles; + } + + public static string SiteKey(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.HasClaim(item => item.Type == "sitekey")) + { + return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value; + } + else + { + return ""; + } + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 06ceaf69..4145f8a0 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -124,7 +124,7 @@ namespace Microsoft.Extensions.DependencyInjection // note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application" services.ConfigureApplicationCookie(options => { - options.Cookie.HttpOnly = false; + options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Events.OnRedirectToLogin = context => @@ -179,7 +179,7 @@ namespace Microsoft.Extensions.DependencyInjection options.Lockout.AllowedForNewUsers = false; // SignIn settings - options.SignIn.RequireConfirmedEmail = true; + options.SignIn.RequireConfirmedEmail = true; options.SignIn.RequireConfirmedPhoneNumber = false; // User settings diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 8ef2f7ad..68a002c7 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -50,7 +50,6 @@ namespace Oqtane.Extensions options.SaveTokens = false; options.GetClaimsFromUserInfoEndpoint = true; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect; - options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // authorization code flow options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure // cookie config is required to avoid Correlation Failed errors @@ -62,6 +61,7 @@ namespace Oqtane.Extensions options.MetadataAddress = sitesettings.GetValue("ExternalLogin:MetadataUrl", ""); options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", ""); options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); + options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { @@ -150,15 +150,16 @@ namespace Oqtane.Extensions private static async Task OnCreatingTicket(OAuthCreatingTicketContext context) { // OAuth 2.0 - var email = ""; - var id = ""; var claims = ""; + var id = ""; + var name = ""; + var email = ""; if (context.Options.UserInformationEndpoint != "") { try { - // call user information endpoint + // call user information endpoint using access token var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); @@ -167,32 +168,49 @@ namespace Oqtane.Extensions response.EnsureSuccessStatusCode(); claims = await response.Content.ReadAsStringAsync(); - // parse json output + // get claim types var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); + var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); - if (!claims.StartsWith("[") && !claims.EndsWith("]")) + + // some user endpoints can return multiple objects (ie. GitHub) so convert single object to array (if necessary) + var jsonclaims = claims; + if (!jsonclaims.StartsWith("[") && !jsonclaims.EndsWith("]")) { - claims = "[" + claims + "]"; // convert to json array + jsonclaims = "[" + jsonclaims + "]"; } - JsonNode items = JsonNode.Parse(claims)!; + + // parse claim values + JsonNode items = JsonNode.Parse(jsonclaims)!; foreach (var item in items.AsArray()) { - if (item[emailClaimType] != null) + name = ""; + email = ""; + + // id claim is required + if (!string.IsNullOrEmpty(idClaimType) && item[idClaimType] != null) { - if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) + id = item[idClaimType].ToString(); + + // name claim is optional + if (!string.IsNullOrEmpty(nameClaimType) && item[nameClaimType] != null) { - email = item[emailClaimType].ToString().ToLower(); - if (item[idClaimType] != null) + name = item[nameClaimType].ToString(); + } + + // email claim is optional + if (!string.IsNullOrEmpty(emailClaimType) && item[emailClaimType] != null) + { + if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) { - id = item[idClaimType].ToString(); + email = item[emailClaimType].ToString().ToLower(); } - break; } } - } - if (string.IsNullOrEmpty(id)) - { - id = email; + if (!string.IsNullOrEmpty(id)) + { + break; + } } } catch (Exception ex) @@ -203,7 +221,7 @@ namespace Oqtane.Extensions } // validate user - var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal); + var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); if (identity.Label == ExternalLoginStatus.Success) { identity.AddClaim(new Claim("access_token", context.AccessToken)); @@ -213,6 +231,14 @@ namespace Oqtane.Extensions // pass properties to OnTicketReceived context.Properties.SetParameter("status", identity.Label); context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri); + + // set cookie expiration + string cookieExpStr = context.HttpContext.GetSiteSettings().GetValue("LoginOptions:CookieExpiration", ""); + if (!string.IsNullOrEmpty(cookieExpStr) && TimeSpan.TryParse(cookieExpStr, out TimeSpan cookieExpTS)) + { + context.Properties.ExpiresUtc = DateTime.Now.Add(cookieExpTS); + context.Properties.IsPersistent = true; + } } private static Task OnTicketReceived(TicketReceivedContext context) @@ -231,28 +257,46 @@ namespace Oqtane.Extensions private static async Task OnTokenValidated(TokenValidatedContext context) { // OpenID Connect + var claims = ""; + var id = ""; + var name = ""; + var email = ""; + + // serialize claims + foreach (var claim in context.Principal.Claims) + { + claims += "\"" + claim.Type + "\":\"" + claim.Value + "\","; + } + claims = "{" + claims.Substring(0, claims.Length - 1) + "}"; + + // get claim types var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); - var id = context.Principal.FindFirstValue(idClaimType); + var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); - var email = context.Principal.FindFirstValue(emailClaimType); - var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray()); + + // parse claim values - id claim is required + id = context.Principal.FindFirstValue(idClaimType); + + // name claim is optional + if (!string.IsNullOrEmpty(nameClaimType) && context.Principal.FindFirstValue(nameClaimType) != null) + { + name = context.Principal.FindFirstValue(nameClaimType); + } + + // email claim is optional + if (!string.IsNullOrEmpty(emailClaimType) && context.Principal.FindFirstValue(emailClaimType) != null) + { + if (EmailValid(context.Principal.FindFirstValue(emailClaimType), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) + { + email = context.Principal.FindFirstValue(emailClaimType); + } + } // validate user - var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal); + var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); if (identity.Label == ExternalLoginStatus.Success) { - // external roles - if (!string.IsNullOrEmpty(context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) - { - foreach (var claim in context.Principal.Claims.Where(item => item.Type == ClaimTypes.Role)) - { - if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value)) - { - identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value)); - } - } - } - + // include access token identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData)); context.Principal = new ClaimsPrincipal(identity); } @@ -284,12 +328,20 @@ namespace Oqtane.Extensions return Task.CompletedTask; } - private static async Task ValidateUser(string email, string id, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) + private static async Task ValidateUser(string id, string name, string email, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) { var _logger = httpContext.RequestServices.GetRequiredService(); ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme); // use identity.Label as a temporary location to store validation status information + // review claims feature (for testing - external login is disabled) + if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false"))) + { + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims); + identity.Label = ExternalLoginStatus.ReviewClaims; + return identity; + } + var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", ""); var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", ""); var alias = httpContext.GetAlias(); @@ -308,136 +360,158 @@ namespace Oqtane.Extensions } else { - if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) + bool duplicates = false; + if (!string.IsNullOrEmpty(email)) { - bool duplicates = false; try { identityuser = await _identityUserManager.FindByEmailAsync(email); } - catch - { - // FindByEmailAsync will throw an error if the email matches multiple user accounts + catch // FindByEmailAsync will throw an error if the email matches multiple user accounts + { duplicates = true; } - if (identityuser == null) + } + if (identityuser == null) + { + if (duplicates) { - if (duplicates) + identity.Label = ExternalLoginStatus.DuplicateEmail; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email); + } + else + { + if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true"))) { - identity.Label = ExternalLoginStatus.DuplicateEmail; - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email); - } - else - { - if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true"))) - { - identityuser = new IdentityUser(); - identityuser.UserName = email; - identityuser.Email = email; - identityuser.EmailConfirmed = true; - var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss", CultureInfo.InvariantCulture)); - if (result.Succeeded) - { - user = new User - { - SiteId = alias.SiteId, - Username = email, - DisplayName = email, - Email = email, - LastLoginOn = null, - LastIPAddress = "" - }; - user = _users.AddUser(user); + // user identifiers + var username = ""; + var emailaddress = ""; + var displayname = ""; + bool emailconfirmed = false; - if (user != null) + if (!string.IsNullOrEmpty(email)) // email claim provided + { + username = email; + emailaddress = email; + displayname = (!string.IsNullOrEmpty(name)) ? name : email; + emailconfirmed = true; + } + else if (!string.IsNullOrEmpty(name)) // name claim provided + { + username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss"); + emailaddress = ""; // unknown - will need to be requested from user later + displayname = name; + } + else // neither email nor name provided + { + username = Guid.NewGuid().ToString("N"); + emailaddress = ""; // unknown - will need to be requested from user later + displayname = username; + } + + identityuser = new IdentityUser(); + identityuser.UserName = username; + identityuser.Email = emailaddress; + identityuser.EmailConfirmed = emailconfirmed; + + // generate password based on random date and punctuation ie. Jan-23-1981+14:43:12! + Random rnd = new Random(); + var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60)); + var password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47); + + var result = await _identityUserManager.CreateAsync(identityuser, password); + if (result.Succeeded) + { + user = new User + { + SiteId = alias.SiteId, + Username = username, + DisplayName = displayname, + Email = emailaddress, + LastLoginOn = null, + LastIPAddress = "" + }; + user = _users.AddUser(user); + + if (user != null) + { + if (!string.IsNullOrEmpty(email)) { var _notifications = httpContext.RequestServices.GetRequiredService(); string url = httpContext.Request.Scheme + "://" + alias.Name; string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; var notification = new Notification(user.SiteId, user, "User Account Notification", body); _notifications.AddNotification(notification); - - // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); - - _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user); - } - else - { - identity.Label = ExternalLoginStatus.UserNotCreated; - _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email); } + + // add user login + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); + + _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user); } else { identity.Label = ExternalLoginStatus.UserNotCreated; - _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString()); + _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email); } } else { - identity.Label = ExternalLoginStatus.UserDoesNotExist; - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email); - } - } - } - else - { - var logins = await _identityUserManager.GetLoginsAsync(identityuser); - var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString())); - if (login == null) - { - if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true"))) - { - // external login using existing user account - verification required - var _notifications = httpContext.RequestServices.GetRequiredService(); - string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - string url = httpContext.Request.Scheme + "://" + alias.Name; - url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}"; - string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. "; - body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; - var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body); - _notifications.AddNotification(notification); - - identity.Label = ExternalLoginStatus.VerificationRequired; - _logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email); - } - else - { - // external login using existing user account - link automatically - user = _users.GetUser(identityuser.UserName); - user.SiteId = alias.SiteId; - - var _notifications = httpContext.RequestServices.GetRequiredService(); - string url = httpContext.Request.Scheme + "://" + alias.Name; - string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; - var notification = new Notification(user.SiteId, user, "User Account Notification", body); - _notifications.AddNotification(notification); - - // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); - - _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName); + identity.Label = ExternalLoginStatus.UserNotCreated; + _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString()); } } else { - // provider keys do not match - identity.Label = ExternalLoginStatus.ProviderKeyMismatch; - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); + identity.Label = ExternalLoginStatus.UserDoesNotExist; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email); } } } - else // email invalid + else { - identity.Label = ExternalLoginStatus.InvalidEmail; - if (!string.IsNullOrEmpty(email)) + var logins = await _identityUserManager.GetLoginsAsync(identityuser); + var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString())); + if (login == null) { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email); + if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true"))) + { + // external login using existing user account - verification required + var _notifications = httpContext.RequestServices.GetRequiredService(); + string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + string url = httpContext.Request.Scheme + "://" + alias.Name; + url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}"; + string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. "; + body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; + var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body); + _notifications.AddNotification(notification); + + identity.Label = ExternalLoginStatus.VerificationRequired; + _logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email); + } + else + { + // external login using existing user account - link automatically + user = _users.GetUser(identityuser.UserName); + user.SiteId = alias.SiteId; + + var _notifications = httpContext.RequestServices.GetRequiredService(); + string url = httpContext.Request.Scheme + "://" + alias.Name; + string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Account Notification", body); + _notifications.AddNotification(notification); + + // add user login + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); + + _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName); + } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims); + // provider keys do not match + identity.Label = ExternalLoginStatus.ProviderKeyMismatch; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); } } } @@ -455,6 +529,25 @@ namespace Oqtane.Extensions user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); _users.UpdateUser(user); + // external roles + if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) + { + if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role)) + { + foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)) + { + if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value)) + { + identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value)); + } + } + } + else + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")); + } + } + // user profile claims if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) { @@ -493,7 +586,7 @@ namespace Oqtane.Extensions } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. The Valid Claims Are {Claims}.", mapping.Split(":")[0], claims); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); } } else @@ -506,9 +599,10 @@ namespace Oqtane.Extensions _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName); } } - else // id invalid + else // claims invalid { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims); + identity.Label = ExternalLoginStatus.MissingClaims; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims); } return identity; diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 66dd2cea..01138129 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -105,12 +105,6 @@ namespace Oqtane.Infrastructure IsNewTenant = false }; - // on upgrade install the associated Nuget package - if (!string.IsNullOrEmpty(install.ConnectionString)) - { - InstallDatabase(install); - } - var installation = IsInstalled(); if (!installation.Success) { @@ -209,57 +203,6 @@ namespace Oqtane.Infrastructure return result; } - private Installation InstallDatabase(InstallConfig install) - { - var result = new Installation {Success = false, Message = string.Empty}; - - try - { - bool installPackages = false; - - // iterate database packages in installation folder - var packagesFolder = new DirectoryInfo(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder)); - foreach (var package in packagesFolder.GetFiles("*.nupkg.bak")) - { - // determine if package needs to be upgraded or installed - bool upgrade = System.IO.File.Exists(package.FullName.Replace(".nupkg.bak",".log")); - if (upgrade || package.Name.StartsWith(Utilities.GetAssemblyName(install.DatabaseType))) - { - var packageName = Path.Combine(package.DirectoryName, package.Name); - packageName = packageName.Substring(0, packageName.IndexOf(".bak")); - package.MoveTo(packageName, true); - installPackages = true; - } - } - if (installPackages) - { - using (var scope = _serviceScopeFactory.CreateScope()) - { - var installationManager = scope.ServiceProvider.GetRequiredService(); - installationManager.InstallPackages(); - } - } - - // load the installation database type (if necessary) - if (Type.GetType(install.DatabaseType) == null) - { - var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); - var assembliesFolder = new DirectoryInfo(assemblyPath); - var assemblyFile = new FileInfo($"{assembliesFolder}/{Utilities.GetAssemblyName(install.DatabaseType)}.dll"); - AssemblyLoadContext.Default.LoadOqtaneAssembly(assemblyFile); - } - - result.Success = true; - } - catch (Exception ex) - { - result.Message = ex.ToString(); - _filelogger.LogError(Utilities.LogMessage(this, result.Message)); - } - - return result; - } - private Installation CreateDatabase(InstallConfig install) { var result = new Installation { Success = false, Message = string.Empty }; @@ -268,8 +211,6 @@ namespace Oqtane.Infrastructure { try { - InstallDatabase(install); - var databaseType = install.DatabaseType; // get database type @@ -436,7 +377,7 @@ namespace Oqtane.Infrastructure } catch (Exception ex) { - result.Message = "An Error Occurred Migrating A Tenant Database. This Is Usually Related To A Tenant Database Not Being In A Supported State. " + ex.ToString(); + result.Message = "An Error Occurred Migrating The Database For Tenant " + tenant.Name + ". This Is Usually Related To Database Permissions, Connection String Mappings, Or The Database Not Being In A Supported State. " + ex.ToString(); _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } @@ -457,7 +398,7 @@ namespace Oqtane.Infrastructure } catch (Exception ex) { - result.Message = "An Error Occurred Executing Upgrade Logic. " + ex.ToString(); + result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString(); _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } } @@ -527,7 +468,7 @@ namespace Oqtane.Infrastructure } catch (Exception ex) { - result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.ToString(); + result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString(); } } } @@ -614,6 +555,7 @@ namespace Oqtane.Infrastructure SiteTemplateType = install.SiteTemplate, Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value, RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value, + HybridEnabled = false }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs b/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs index 41dab17d..5bf1fd14 100644 --- a/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs +++ b/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs @@ -15,10 +15,18 @@ namespace Oqtane.Infrastructure.EventSubscribers public void EntityChanged(SyncEvent syncEvent) { + // when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh) { _cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}"); } + + // when a site entity is updated the hosting model may have changed, so the client assemblies cache items need to be refreshed + if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Update) + { + _cache.Remove($"assemblieslist:{syncEvent.TenantId}:{syncEvent.EntityId}"); + _cache.Remove($"assemblies:{syncEvent.TenantId}:{syncEvent.EntityId}"); + } } } } diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index f29b3aba..0d26600b 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -13,7 +13,6 @@ using System.Xml; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Oqtane.Controllers; using Oqtane.Shared; // ReSharper disable AssignNullToNotNullAttribute @@ -41,51 +40,25 @@ namespace Oqtane.Infrastructure } } + // method must be static as it is called in ConfigureServices during Startup public static string InstallPackages(string webRootPath, string contentRootPath) { string errors = ""; string binPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); - string sourceFolder = Path.Combine(contentRootPath, "Packages"); + string sourceFolder = Path.Combine(contentRootPath, Constants.PackagesFolder); if (!Directory.Exists(sourceFolder)) { Directory.CreateDirectory(sourceFolder); } - // move packages to secure /Packages folder - foreach (var folderName in "Modules,Themes,Packages".Split(",")) - { - string folder = Path.Combine(webRootPath, folderName); - if (Directory.Exists(folder)) - { - foreach (var file in Directory.GetFiles(folder, "*.nupkg*")) - { - var destinationFile = Path.Combine(sourceFolder, Path.GetFileName(file)); - if (File.Exists(destinationFile)) - { - File.Delete(destinationFile); - } + // read assembly log + var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log"); + var assemblies = GetAssemblyLog(assemblyLogPath); - if (destinationFile.ToLower().EndsWith(".nupkg.bak")) - { - // leave a copy in the current folder as it is distributed with the core framework - File.Copy(file, destinationFile); - } - else - { - // move to destination - File.Move(file, destinationFile); - } - } - } - else - { - Directory.CreateDirectory(folder); - } - } - - // iterate through Nuget packages in source folder - foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg")) + // install Nuget packages in secure Packages folder + var packages = Directory.GetFiles(sourceFolder, "*.nupkg"); + foreach (string packagename in packages) { try { @@ -154,10 +127,29 @@ namespace Oqtane.Infrastructure // ContentRootPath sometimes produces inconsistent path casing - so can't use string.Replace() filename = Regex.Replace(filename, Regex.Escape(contentRootPath), "", RegexOptions.IgnoreCase); assets.Add(filename); - if (!manifest && Path.GetExtension(filename) == ".log") + + // packages can include a manifest (rather than relying on the framework to dynamically create one) + if (!manifest && filename.EndsWith(name + ".log")) { manifest = true; } + + // register assembly + if (Path.GetExtension(filename) == ".dll") + { + // if package version was not installed previously + if (!File.Exists(Path.Combine(sourceFolder, name + ".log"))) + { + if (assemblies.ContainsKey(Path.GetFileName(filename))) + { + assemblies[Path.GetFileName(filename)] += 1; + } + else + { + assemblies.Add(Path.GetFileName(filename), 1); + } + } + } } } @@ -187,6 +179,12 @@ namespace Oqtane.Infrastructure File.Delete(packagename); } + if (packages.Length != 0) + { + // save assembly log + SetAssemblyLog(assemblyLogPath, assemblies); + } + return errors; } @@ -232,6 +230,10 @@ namespace Oqtane.Infrastructure { if (!string.IsNullOrEmpty(PackageName)) { + // read assembly log + var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log"); + var assemblies = GetAssemblyLog(assemblyLogPath); + // get manifest with highest version string packagename = ""; string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log"); @@ -249,17 +251,31 @@ namespace Oqtane.Infrastructure { // legacy support for assets that were stored as absolute paths string filepath = asset.StartsWith("\\") ? Path.Combine(_environment.ContentRootPath, asset.Substring(1)) : asset; - if (File.Exists(filepath)) + + // delete assets + if (Path.GetExtension(filepath) == ".dll") { - // do not remove licensing assemblies - this is a temporary fix until a more robust dependency management solution is available - if (!filepath.Contains("Oqtane.Licensing.")) + // use assembly log to determine if assembly is used in other packages + if (assemblies.ContainsKey(Path.GetFileName(filepath))) { - File.Delete(filepath); - if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any()) + if (assemblies[Path.GetFileName(filepath)] == 1) { - Directory.Delete(Path.GetDirectoryName(filepath), true); + DeleteFile(filepath); + assemblies.Remove(Path.GetFileName(filepath)); + } + else + { + assemblies[Path.GetFileName(filepath)] -= 1; } } + else // does not exist in assembly log + { + DeleteFile(filepath); + } + } + else // not an assembly + { + DeleteFile(filepath); } } @@ -269,6 +285,9 @@ namespace Oqtane.Infrastructure File.Delete(asset); } + // save assembly log + SetAssemblyLog(assemblyLogPath, assemblies); + return true; } } @@ -276,6 +295,76 @@ namespace Oqtane.Infrastructure return false; } + private void DeleteFile(string filepath) + { + if (File.Exists(filepath)) + { + File.Delete(filepath); + if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any()) + { + Directory.Delete(Path.GetDirectoryName(filepath), true); + } + } + } + + public int RegisterAssemblies() + { + var assemblyLogPath = GetAssemblyLogPath(); + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + var assemblies = GetAssemblyLog(assemblyLogPath); + + // remove assemblies that no longer exist + foreach (var dll in assemblies) + { + if (!File.Exists(Path.Combine(binFolder, dll.Key))) + { + assemblies.Remove(dll.Key); + } + } + // add assemblies which are not registered + foreach (var dll in Directory.GetFiles(binFolder, "*.dll")) + { + if (!assemblies.ContainsKey(Path.GetFileName(dll))) + { + assemblies.Add(Path.GetFileName(dll), 1); + } + } + + SetAssemblyLog(assemblyLogPath, assemblies); + + return assemblies.Count; + } + + private string GetAssemblyLogPath() + { + string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); + if (!Directory.Exists(packagesFolder)) + { + Directory.CreateDirectory(packagesFolder); + } + return Path.Combine(packagesFolder, "assemblies.log"); + } + + private static Dictionary GetAssemblyLog(string assemblyLogPath) + { + Dictionary assemblies = new Dictionary(); + if (File.Exists(assemblyLogPath)) + { + assemblies = JsonSerializer.Deserialize>(File.ReadAllText(assemblyLogPath)); + } + return assemblies; + } + + private static void SetAssemblyLog(string assemblyLogPath, Dictionary assemblies) + { + if (File.Exists(assemblyLogPath)) + { + File.Delete(assemblyLogPath); + } + File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true })); + } + public async Task UpgradeFramework() { string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs index da4a6604..0e5bcc6d 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs @@ -6,6 +6,7 @@ namespace Oqtane.Infrastructure { void InstallPackages(); bool UninstallPackage(string PackageName); + int RegisterAssemblies(); Task UpgradeFramework(); void RestartApplication(); } diff --git a/Oqtane.Server/Infrastructure/Interfaces/ITenantManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ITenantManager.cs index b1a4892b..c4388714 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/ITenantManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/ITenantManager.cs @@ -7,6 +7,7 @@ namespace Oqtane.Infrastructure Alias GetAlias(); Tenant GetTenant(); void SetAlias(Alias alias); + void SetAlias(int tenantId, int siteId); void SetTenant(int tenantId); } } diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index ff4a88f8..28a13387 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure var logRepository = provider.GetRequiredService(); var visitorRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); + var installationManager = provider.GetRequiredService(); // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); @@ -96,6 +97,17 @@ namespace Oqtane.Infrastructure } } + // register assemblies + try + { + var assemblies = installationManager.RegisterAssemblies(); + log += assemblies.ToString() + " Assemblies Registered
"; + } + catch (Exception ex) + { + log += $"Error Registering Assemblies - {ex.Message}
"; + } + return log; } diff --git a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs index 0c415f85..dd9e97dd 100644 --- a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs +++ b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Oqtane.Repository; using Oqtane.Shared; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace Oqtane.Infrastructure { @@ -22,8 +23,7 @@ namespace Oqtane.Infrastructure var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; string path = context.Request.Path.ToString(); - - if (config.IsInstalled() && !path.StartsWith("/_blazor")) + if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests { // get alias (note that this also sets SiteState.Alias) var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager; @@ -57,9 +57,25 @@ namespace Oqtane.Infrastructure { if (path.StartsWith("/" + alias.Path) && (Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")))) { - context.Request.Path = path.Replace("/" + alias.Path, ""); + context.Request.Path = path.Substring(alias.Path.Length + 1); } } + + // handle sitemap.xml request + if (context.Request.Path.ToString().Contains("/sitemap.xml") && !context.Request.Path.ToString().Contains("/pages")) + { + context.Request.Path = "/pages/sitemap.xml"; + } + + // handle robots.txt root request (does not support subfolder aliases) + if (context.Request.Path.StartsWithSegments("/robots.txt") && string.IsNullOrEmpty(alias.Path)) + { + // allow all user agents and specify site map + var robots = $"User-agent: *\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml"; + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync(robots); + return; + } } } diff --git a/Oqtane.Server/Infrastructure/TenantManager.cs b/Oqtane.Server/Infrastructure/TenantManager.cs index 7134e135..92191f4e 100644 --- a/Oqtane.Server/Infrastructure/TenantManager.cs +++ b/Oqtane.Server/Infrastructure/TenantManager.cs @@ -26,13 +26,14 @@ namespace Oqtane.Infrastructure { Alias alias = null; - if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1) + // does not support mock Alias objects (GetTenant should be used to retrieve a TenantId) + if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1) { alias = _siteState.Alias; } else { - // if there is http context + // if there is HttpContext var httpcontext = _httpContextAccessor.HttpContext; if (httpcontext != null) { @@ -78,15 +79,19 @@ namespace Oqtane.Infrastructure return null; } + // background processes can set the alias using the SiteState service public void SetAlias(Alias alias) { - // background processes can set the alias using the SiteState service _siteState.Alias = alias; } + public void SetAlias(int tenantId, int siteId) + { + _siteState.Alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.TenantId == tenantId && item.SiteId == siteId); + } + public void SetTenant(int tenantId) { - // background processes can set the alias using the SiteState service _siteState.Alias = new Alias { TenantId = tenantId, AliasId = -1, SiteId = -1 }; } } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index dc9c596d..4d273be7 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -106,7 +106,7 @@ namespace Oqtane.Managers { if (string.IsNullOrEmpty(user.Password)) { - // create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12! + // generate password based on random date and punctuation ie. Jan-23-1981+14:43:12! Random rnd = new Random(); var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60)); user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47); @@ -152,7 +152,7 @@ namespace Oqtane.Managers { string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; + string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; var notification = new Notification(user.SiteId, User, "User Account Verification", body); _notifications.AddNotification(notification); } @@ -205,8 +205,22 @@ namespace Oqtane.Managers if (user.Email != identityuser.Email) { await _identityUserManager.SetEmailAsync(identityuser, user.Email); - var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + + // if email address changed and user is not administrator, email verification is required for new email address + if (!user.EmailConfirmed) + { + var alias = _tenantManager.GetAlias(); + string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); + } + else + { + var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + } } user = _users.UpdateUser(user); @@ -308,7 +322,7 @@ namespace Oqtane.Managers user = _users.GetUser(identityuser.UserName); if (user != null) { - if (identityuser.EmailConfirmed) + if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) { user.IsAuthenticated = true; user.LastLoginOn = DateTime.UtcNow; @@ -323,7 +337,7 @@ namespace Oqtane.Managers } else { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); } } } diff --git a/Oqtane.Server/Migrations/Tenant/05000100_AddSiteHybridEnabled.cs b/Oqtane.Server/Migrations/Tenant/05000100_AddSiteHybridEnabled.cs new file mode 100644 index 00000000..9f39a4d7 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/05000100_AddSiteHybridEnabled.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.05.00.01.00")] + public class AddSiteHybridEnabled : MultiDatabaseMigration + { + public AddSiteHybridEnabled(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddBooleanColumn("HybridEnabled", true); + siteEntityBuilder.UpdateColumn("HybridEnabled", "0", "bool", ""); // default to false + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.DropColumn("HybridEnabled"); + } + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index b1ddd42d..3b7d540a 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.0.0 + 5.0.1 Oqtane Shaun Walker .NET Foundation @@ -11,13 +11,14 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git Oqtane true $(DefineConstants);OQTANE;OQTANE3 true + none @@ -35,7 +36,6 @@ - all @@ -43,9 +43,10 @@ - + + @@ -55,10 +56,18 @@ + + + + + + + + diff --git a/Oqtane.Server/Pages/External.cshtml.cs b/Oqtane.Server/Pages/External.cshtml.cs index 49c728f0..25a9be2c 100644 --- a/Oqtane.Server/Pages/External.cshtml.cs +++ b/Oqtane.Server/Pages/External.cshtml.cs @@ -19,7 +19,7 @@ namespace Oqtane.Pages var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", ""); if (providertype != "") { - return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" }); + return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" }); } else { diff --git a/Oqtane.Server/Pages/Sitemap.cshtml.cs b/Oqtane.Server/Pages/Sitemap.cshtml.cs index 0e0d09f4..461bc3d6 100644 --- a/Oqtane.Server/Pages/Sitemap.cshtml.cs +++ b/Oqtane.Server/Pages/Sitemap.cshtml.cs @@ -53,7 +53,8 @@ namespace Oqtane.Pages { if (_userPermissions.IsAuthorized(null, PermissionNames.View, page.PermissionList) && page.IsNavigation) { - sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow }); + var rooturl = _alias.Protocol + (string.IsNullOrEmpty(_alias.Path) ? _alias.Name : _alias.Name.Substring(0, _alias.Name.IndexOf("/"))); + sitemap.Add(new Sitemap { Url = rooturl + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow }); foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) { @@ -72,7 +73,7 @@ namespace Oqtane.Pages var urls = ((ISitemap)moduleobject).GetUrls(_alias.Path, page.Path, pageModule.Module); foreach (var url in urls) { - sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + url.Url, ModifiedOn = DateTime.UtcNow }); + sitemap.Add(new Sitemap { Url = rooturl + url.Url, ModifiedOn = DateTime.UtcNow }); } } catch (Exception ex) diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 50fb2575..a451f5a0 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -505,7 +505,7 @@ namespace Oqtane.Pages { if (resource.Url.StartsWith("~")) { - resource.Url = resource.Url.Replace("~", "/Themes/" + name + "/").Replace("//", "/"); + resource.Url = resource.Url.Replace("~", "/Themes/" + Utilities.GetTypeName(name) + "/").Replace("//", "/"); } if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { diff --git a/Oqtane.Server/Program.cs b/Oqtane.Server/Program.cs index 2d762efd..bd6a9471 100644 --- a/Oqtane.Server/Program.cs +++ b/Oqtane.Server/Program.cs @@ -4,9 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore; using Microsoft.Extensions.DependencyInjection; using Oqtane.Infrastructure; -using System.Diagnostics; using Microsoft.Extensions.Logging; -using Oqtane.Shared; using Oqtane.Documentation; namespace Oqtane.Server diff --git a/Oqtane.Server/Repository/Context/DBContextBase.cs b/Oqtane.Server/Repository/Context/DBContextBase.cs index 0b9bf821..fc02291d 100644 --- a/Oqtane.Server/Repository/Context/DBContextBase.cs +++ b/Oqtane.Server/Repository/Context/DBContextBase.cs @@ -45,13 +45,20 @@ namespace Oqtane.Repository Tenant tenant = _tenantManager.GetTenant(); if (tenant != null) { - _connectionString = _config.GetConnectionString(tenant.DBConnectionString) - .Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString()); - _databaseType = tenant.DBType; + _connectionString = _config.GetConnectionString(tenant.DBConnectionString); + if (_connectionString != null) + { + _connectionString = _connectionString.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString()); + _databaseType = tenant.DBType; + } + else + { + // tenant connection string does not exist in appsettings.json + } } } - if (!String.IsNullOrEmpty(_databaseType)) + if (!string.IsNullOrEmpty(_databaseType)) { var type = Type.GetType(_databaseType); ActiveDatabase = Activator.CreateInstance(type) as IDatabase; diff --git a/Oqtane.Server/Repository/Interfaces/ILanguageRepository.cs b/Oqtane.Server/Repository/Interfaces/ILanguageRepository.cs index 0ec10442..14619121 100644 --- a/Oqtane.Server/Repository/Interfaces/ILanguageRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/ILanguageRepository.cs @@ -9,6 +9,8 @@ namespace Oqtane.Repository Language AddLanguage(Language language); + void UpdateLanguage(Language language); + Language GetLanguage(int languageId); void DeleteLanguage(int languageId); diff --git a/Oqtane.Server/Repository/LanguageRepository.cs b/Oqtane.Server/Repository/LanguageRepository.cs index fb252fd6..80dc01ad 100644 --- a/Oqtane.Server/Repository/LanguageRepository.cs +++ b/Oqtane.Server/Repository/LanguageRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore; using Oqtane.Models; namespace Oqtane.Repository @@ -12,7 +13,7 @@ namespace Oqtane.Repository { _db = context; } - + public IEnumerable GetLanguages(int siteId) { return _db.Language.Where(l => l.SiteId == siteId); @@ -35,6 +36,25 @@ namespace Oqtane.Repository return language; } + public void UpdateLanguage(Language language) + { + if (language.LanguageId != 0) + { + _db.Entry(language).State = EntityState.Modified; + } + if (language.IsDefault) + { + // Ensure all other languages are not set to default + _db.Language + .Where(l => l.SiteId == language.SiteId && + l.LanguageId != language.LanguageId) + .ToList() + .ForEach(l => l.IsDefault = false); + } + + _db.SaveChanges(); + } + public Language GetLanguage(int languageId) { return _db.Language.Find(languageId); diff --git a/Oqtane.Server/Repository/SqlRepository.cs b/Oqtane.Server/Repository/SqlRepository.cs index 31e0aa10..a4689fcf 100644 --- a/Oqtane.Server/Repository/SqlRepository.cs +++ b/Oqtane.Server/Repository/SqlRepository.cs @@ -93,7 +93,15 @@ namespace Oqtane.Repository public int ExecuteNonQuery(string connectionString, string databaseType, string query) { var db = GetActiveDatabase(databaseType); - return db.ExecuteNonQuery(GetConnectionString(connectionString), query); + var connectionstring = GetConnectionString(connectionString); + if (connectionstring != null) + { + return db.ExecuteNonQuery(GetConnectionString(connectionString), query); + } + else + { + return 0; + } } public string GetScriptFromAssembly(Assembly assembly, string fileName) diff --git a/Oqtane.Server/Security/JwtManager.cs b/Oqtane.Server/Security/JwtManager.cs index a73a3d6c..026071a1 100644 --- a/Oqtane.Server/Security/JwtManager.cs +++ b/Oqtane.Server/Security/JwtManager.cs @@ -18,7 +18,7 @@ namespace Oqtane.Security public string GenerateToken(Alias alias, ClaimsIdentity identity, string secret, string issuer, string audience, int lifetime) { var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(secret); + var key = Encoding.ASCII.GetBytes(PadSecret(secret)); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(identity), @@ -36,7 +36,7 @@ namespace Oqtane.Security if (!string.IsNullOrEmpty(token)) { var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(secret); + var key = Encoding.ASCII.GetBytes(PadSecret(secret)); try { tokenHandler.ValidateToken(token, new TokenValidationParameters @@ -66,5 +66,11 @@ namespace Oqtane.Security } return null; } + + private string PadSecret(string secret) + { + // ensure secret is 256 bits + return (secret.Length < 32) ? (secret + "????????????????????????????????").Substring(0, 32) : secret; + } } } diff --git a/Oqtane.Server/Security/PrincipalValidator.cs b/Oqtane.Server/Security/PrincipalValidator.cs index 5a15a2b2..269fbc5b 100644 --- a/Oqtane.Server/Security/PrincipalValidator.cs +++ b/Oqtane.Server/Security/PrincipalValidator.cs @@ -8,6 +8,7 @@ using Oqtane.Models; using System.Collections.Generic; using Oqtane.Extensions; using Oqtane.Shared; +using System.IO; namespace Oqtane.Security { @@ -17,9 +18,11 @@ namespace Oqtane.Security { if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null) { - // check if framework is installed var config = context.HttpContext.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; - if (config.IsInstalled()) + string path = context.Request.Path.ToString().ToLower(); + + // check if framework is installed + if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests { // get current site var alias = context.HttpContext.GetAlias(); @@ -28,12 +31,11 @@ namespace Oqtane.Security var claims = context.Principal.Claims; // check if principal has roles and matches current site - if (!claims.Any(item => item.Type == ClaimTypes.Role) || claims.FirstOrDefault(item => item.Type == "sitekey")?.Value != alias.SiteKey) + if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey)) { var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository; var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager; - string path = context.Request.Path.ToString().ToLower(); User user = userRepository.GetUser(context.Principal.Identity.Name); if (user != null) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 8d8026d6..da191dcc 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -71,6 +71,9 @@ namespace Oqtane { options.DetailedErrors = true; } + }) + .AddHubOptions(options => { + options.MaximumReceiveMessageSize = null; // no limit (for large amnounts of data ie. textarea components) }); // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd index 14ce91dc..2fd1234c 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd @@ -1,3 +1,4 @@ +del "*.nupkg" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs index 90384aff..559f9697 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs @@ -81,7 +81,7 @@ namespace [Owner].Module.[Module].Controllers [Authorize(Policy = PolicyNames.EditModule)] public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module]) { - if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null) + if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null) { [Module] = _[Module]Repository.Update[Module]([Module]); _logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]); diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg similarity index 100% rename from Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak rename to Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg similarity index 100% rename from Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak rename to Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg similarity index 100% rename from Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak rename to Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg similarity index 100% rename from Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak rename to Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css index de27f0cb..654247d4 100644 --- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css @@ -4,6 +4,12 @@ body { padding-top: 7rem; } +/* App Logo */ +.app-logo .img-fluid { + max-height: 90px; + padding: 0 5px 0 5px; +} + .table > :not(caption) > * > * { box-shadow: none; } diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css index 129a0b9c..fa0bd479 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css @@ -4,6 +4,12 @@ body { padding-top: 7rem; } +/* App Logo */ +.app-logo .img-fluid { + max-height: 90px; + padding: 0 5px 0 5px; +} + .table > :not(caption) > * > * { box-shadow: none; } diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd index adb7ee28..ae35eae9 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd @@ -1,2 +1,3 @@ +del "*.nupkg" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Theme.[Theme].nuspec XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\wwwroot\Themes\" /Y diff --git a/Oqtane.Shared/Models/Alias.cs b/Oqtane.Shared/Models/Alias.cs index 17ec6e80..1cc800c0 100644 --- a/Oqtane.Shared/Models/Alias.cs +++ b/Oqtane.Shared/Models/Alias.cs @@ -46,7 +46,7 @@ namespace Oqtane.Models { get { - if (Name.Contains("/")) + if (Name != null && Name.Contains("/")) { return Name.Substring(Name.IndexOf("/") + 1); } @@ -58,7 +58,7 @@ namespace Oqtane.Models } /// - /// Unique key used for identifying a site within a runtime process (ie. cache, etc...) + /// Unique key used for identifying a site within a runtime process (ie. cache, file system, etc...) /// [NotMapped] public string SiteKey diff --git a/Oqtane.Shared/Models/Result.cs b/Oqtane.Shared/Models/Result.cs new file mode 100644 index 00000000..d550f2c4 --- /dev/null +++ b/Oqtane.Shared/Models/Result.cs @@ -0,0 +1,21 @@ +namespace Oqtane.Models +{ + public class Result + { + public bool Success { get; set; } + + public string Message { get; set; } + + public Result(bool success) + { + Success = success; + Message = ""; + } + + public Result(bool success, string message) + { + Success = success; + Message = message; + } + } +} diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 66d171c9..30c5c02f 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -78,6 +78,11 @@ namespace Oqtane.Models /// public string RenderMode { get; set; } + /// + /// Indicates if a site can be integrated with an external .NET MAUI hybrid application + /// + public bool HybridEnabled { get; set; } + /// /// Keeps track of site configuration changes and is used by the ISiteMigration interface /// @@ -98,6 +103,18 @@ namespace Oqtane.Models /// public string BodyContent { get; set; } + /// + /// The ImageFile extensions + /// + [NotMapped] + public string ImageFiles { get; set; } + + /// + /// The UploadableFile extensions + /// + [NotMapped] + public string UploadableFiles { get; set; } + [NotMapped] public Dictionary Settings { get; set; } diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index ad830b82..265cc578 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.0.0 + 5.0.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/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 45bb7c91..d0b44394 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -7,8 +7,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "5.0.0"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0"; + public static readonly string Version = "5.0.1"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; diff --git a/Oqtane.Shared/Shared/ExternalLoginStatus.cs b/Oqtane.Shared/Shared/ExternalLoginStatus.cs index aec49985..63cd0094 100644 --- a/Oqtane.Shared/Shared/ExternalLoginStatus.cs +++ b/Oqtane.Shared/Shared/ExternalLoginStatus.cs @@ -1,7 +1,7 @@ namespace Oqtane.Shared { public class ExternalLoginStatus { public const string Success = "Success"; - public const string InvalidEmail = "InvalidEmail"; + public const string MissingClaims = "MissingClaims"; public const string DuplicateEmail = "DuplicateEmail"; public const string UserNotCreated = "UserNotCreated"; public const string UserDoesNotExist = "UserDoesNotExist"; @@ -9,5 +9,6 @@ namespace Oqtane.Shared { public const string VerificationRequired = "VerificationRequired"; public const string AccessDenied = "AccessDenied"; public const string RemoteFailure = "RemoteFailure"; + public const string ReviewClaims = "ReviewClaims"; } } diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 6218d42f..bc4faa12 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net8.0 Exe - 5.0.0 + 5.0.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/v5.0.0 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.sln b/Oqtane.sln index f6d07ccb..bae318dd 100644 --- a/Oqtane.sln +++ b/Oqtane.sln @@ -16,6 +16,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.MySQL", "Oqtane.Database.MySQL\Oqtane.Database.MySQL.csproj", "{17082878-FA9B-4F08-BC9A-467C23560AEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.PostgreSQL", "Oqtane.Database.PostgreSQL\Oqtane.Database.PostgreSQL.csproj", "{C54A9201-2EA6-499A-BEE8-9E7EE80B9FEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.Sqlite", "Oqtane.Database.Sqlite\Oqtane.Database.Sqlite.csproj", "{61754121-6D69-4B99-B28F-03C8854732DB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.SqlServer", "Oqtane.Database.SqlServer\Oqtane.Database.SqlServer.csproj", "{C1BD386B-0832-41D2-8E37-410127318556}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,6 +42,22 @@ Global {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Release|Any CPU.Build.0 = Release|Any CPU + {17082878-FA9B-4F08-BC9A-467C23560AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17082878-FA9B-4F08-BC9A-467C23560AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17082878-FA9B-4F08-BC9A-467C23560AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17082878-FA9B-4F08-BC9A-467C23560AEB}.Release|Any CPU.Build.0 = Release|Any CPU + {C54A9201-2EA6-499A-BEE8-9E7EE80B9FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C54A9201-2EA6-499A-BEE8-9E7EE80B9FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C54A9201-2EA6-499A-BEE8-9E7EE80B9FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C54A9201-2EA6-499A-BEE8-9E7EE80B9FEE}.Release|Any CPU.Build.0 = Release|Any CPU + {61754121-6D69-4B99-B28F-03C8854732DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61754121-6D69-4B99-B28F-03C8854732DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61754121-6D69-4B99-B28F-03C8854732DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61754121-6D69-4B99-B28F-03C8854732DB}.Release|Any CPU.Build.0 = Release|Any CPU + {C1BD386B-0832-41D2-8E37-410127318556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1BD386B-0832-41D2-8E37-410127318556}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1BD386B-0832-41D2-8E37-410127318556}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1BD386B-0832-41D2-8E37-410127318556}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index fecbecf1..7d6922eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Latest Release -[4.0.6](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6) was released on Oct 16, 2023 and is primarily focused on stabilization. This release includes 34 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 4200. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[5.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0) was released on Nov 16, 2023 and is a major release targeted at .NET 8. This release includes 45 pull requests by 4 different contributors, pushing the total number of project commits all-time to over 4300. 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) @@ -16,11 +16,11 @@ Please note that this project is owned by the .NET Foundation and is governed by # Getting Started -**Using Version 4:** +**Using Version 5:** -- Install **[.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0)**. +- Install **[.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**. -- Install the latest edition (v17.0 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/vs/preview/#download-preview) 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**. +- Install the latest edition (v17.8 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**. - clone the Oqtane dev branch source code to your local system. Open the **Oqtane.sln** solution file and Build the solution. Make sure you specify Oqtane.Server as the Startup Project and then Run the application. @@ -50,8 +50,11 @@ Backlog (TBD) - [ ] Folder Providers - [ ] Generative AI Integration -5.0.0 (Q4 2023) -- [ ] Migration to .NET 8 +5.1.0 (Q1 2024) +- [ ] Full Stack Blazor (Static Server-Side Rendering) + +[5.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0) (Nov 16, 2023) +- [x] Migration to .NET 8 [4.0.6](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6) ( Oct 16, 2023 ) - [x] Stabilization improvements @@ -223,6 +226,8 @@ Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalke # Release Announcements +[Oqtane 5.0](https://www.oqtane.org/blog/!/75/announcing-oqtane-5-0-for-net-8) + [Oqtane 4.0](https://www.oqtane.org/blog/!/63/announcing-oqtane-4-0-for-net-7) [Oqtane 3.4](https://www.oqtane.org/blog/!/56/oqtane-3-4-0-released)