diff --git a/LICENSE b/LICENSE index 9866f46b..d2b14c41 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2023 .NET Foundation +Copyright (c) 2018-2024 .NET Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index eb4ab1a8..7e223ca0 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -69,7 +69,7 @@

@Localizer["ApplicationAdmin"]


- +
diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index 41926187..848ab8e5 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -31,7 +31,7 @@
- +
- +
diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 7f4f35f1..b1ed379a 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -48,6 +48,18 @@
+
+ +
+ +
+
+
+ +
+ +
+
@@ -114,7 +126,7 @@ -@code { + @code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Module Settings"; @@ -141,7 +153,8 @@ private DateTime createdon; private string modifiedby; private DateTime modifiedon; - + private DateTime? _effectivedate = null; + private DateTime? _expirydate = null; protected override void OnInitialized() { _module = ModuleState.ModuleDefinition.Name; @@ -156,6 +169,8 @@ createdon = ModuleState.CreatedOn; modifiedby = ModuleState.ModifiedBy; modifiedon = ModuleState.ModifiedOn; + _effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate); + _expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate); if (ModuleState.ModuleDefinition != null) { @@ -214,12 +229,20 @@ var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { + if (!string.IsNullOrEmpty(_title)) { + if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) + { + AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); + return; + } var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); pagemodule.PageId = int.Parse(_pageId); pagemodule.Title = _title; pagemodule.Pane = _pane; + pagemodule.EffectiveDate = Utilities.LocalDateAndTimeAsUtc(_effectivedate); + pagemodule.ExpiryDate = Utilities.LocalDateAndTimeAsUtc(_expirydate); pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty; if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType) { @@ -269,5 +292,4 @@ AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); } } - } diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 6e95fdf3..05a97d7a 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -119,6 +119,18 @@
+
+ +
+ +
+
+
+ +
+ +
+
@@ -233,6 +245,8 @@ protected Page _parent = null; protected Dictionary _icons; private string _iconresources = ""; + private DateTime? _effectivedate = null; + private DateTime? _expirydate = null; protected override async Task OnInitializedAsync() { @@ -265,6 +279,8 @@ _children.Add(p); } } + _effectivedate = Utilities.UtcAsLocalDate(PageState.Page.EffectiveDate); + _expirydate = Utilities.UtcAsLocalDate(PageState.Page.ExpiryDate); ThemeSettings(); _initialized = true; } @@ -274,18 +290,18 @@ AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error); } } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading Page {Error}", ex.Message); - AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error); - } - } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Page {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error); + } + } - private async void ParentChanged(ChangeEventArgs e) - { - try - { - _parentid = (string)e.Value; + private async void ParentChanged(ChangeEventArgs e) + { + try + { + _parentid = (string)e.Value; _children = new List(); foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) { @@ -295,13 +311,13 @@ } } StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message); - AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error); - } - } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message); + AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error); + } + } private void ThemeChanged(ChangeEventArgs e) { @@ -318,109 +334,116 @@ } } - private void ThemeSettings() - { - _themeSettingsType = null; + private void ThemeSettings() + { + _themeSettingsType = null; var theme = PageState.Site.Themes.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); - if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) - { - _themeSettingsType = Type.GetType(theme.ThemeSettingsType); - if (_themeSettingsType != null) - { - ThemeSettingsComponent = builder => - { - builder.OpenComponent(0, _themeSettingsType); - builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - builder.CloseComponent(); - }; - } - _refresh = true; - } - } + if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) + { + _themeSettingsType = Type.GetType(theme.ThemeSettingsType); + if (_themeSettingsType != null) + { + ThemeSettingsComponent = builder => + { + builder.OpenComponent(0, _themeSettingsType); + builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); + builder.CloseComponent(); + }; + } + _refresh = true; + } + } - private async Task SavePage() - { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) - { - Page page = null; - try - { + private async Task SavePage() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + Page page = null; + try + { + if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) + { + AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); + return; + } if (!string.IsNullOrEmpty(_themetype) && !string.IsNullOrEmpty(_containertype)) - { - page = new Page(); - page.SiteId = PageState.Page.SiteId; - page.Name = _name; + { + page = new Page(); + page.SiteId = PageState.Page.SiteId; + page.Name = _name; - if (string.IsNullOrEmpty(_path)) - { - _path = _name; - } - if (_path.Contains("/")) - { - if (_path.EndsWith("/") && _path != "/") - { - _path = _path.Substring(0, _path.Length - 1); - } - _path = _path.Substring(_path.LastIndexOf("/") + 1); - } + if (string.IsNullOrEmpty(_path)) + { + _path = _name; + } + if (_path.Contains("/")) + { + if (_path.EndsWith("/") && _path != "/") + { + _path = _path.Substring(0, _path.Length - 1); + } + _path = _path.Substring(_path.LastIndexOf("/") + 1); + } - if (_parentid == "-1") - { - page.ParentId = null; - page.Path = Utilities.GetFriendlyUrl(_path); - } - else - { - page.ParentId = Int32.Parse(_parentid); - var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault(); - if (parent.Path == string.Empty) - { - page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path); - } - else - { - page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path); - } - } + if (_parentid == "-1") + { + page.ParentId = null; + page.Path = Utilities.GetFriendlyUrl(_path); + } + else + { + page.ParentId = Int32.Parse(_parentid); + var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault(); + if (parent.Path == string.Empty) + { + page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path); + } + else + { + page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path); + } + } - var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); - if (_pages.Any(item => item.Path == page.Path)) - { - AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning); - return; - } + var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); + if (_pages.Any(item => item.Path == page.Path)) + { + AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning); + return; + } - if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower())) - { - AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning); - return; - } + if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower())) + { + AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning); + return; + } - Page child; - switch (_insert) - { - case "<<": - page.Order = 0; - break; - case "<": - child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); - page.Order = child.Order - 1; - break; - case ">": - child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); - page.Order = child.Order + 1; - break; - case ">>": - page.Order = int.MaxValue; - break; - } + Page child; + switch (_insert) + { + case "<<": + page.Order = 0; + break; + case "<": + child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); + page.Order = child.Order - 1; + break; + case ">": + child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); + page.Order = child.Order + 1; + break; + case ">>": + page.Order = int.MaxValue; + break; + } - page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation)); - page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable)); + page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation)); + page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable)); page.Url = _url; page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable)); + page.EffectiveDate = Utilities.LocalDateAndTimeAsUtc(_effectivedate); + page.ExpiryDate = Utilities.LocalDateAndTimeAsUtc(_expirydate); page.UserId = null; // appearance diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 222d0119..3084ef84 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -134,6 +134,18 @@
+
+ +
+ +
+
+
+ +
+ +
+
@@ -322,6 +334,8 @@ protected Page _parent = null; protected Dictionary _icons; private string _iconresources = ""; + private DateTime? _effectivedate = null; + private DateTime? _expirydate = null; protected override async Task OnInitializedAsync() { @@ -370,6 +384,8 @@ } _url = _page.Url; _icon = _page.Icon; + _effectivedate = Utilities.UtcAsLocalDate(_page.EffectiveDate); + _expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate); _ispersonalizable = _page.IsPersonalizable.ToString(); // appearance @@ -487,6 +503,11 @@ { try { + if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) + { + AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); + return; + } if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") { string currentPath = _page.Path; @@ -563,6 +584,8 @@ _page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable)); _page.Url = _url; _page.Icon = _icon ?? string.Empty; + _page.EffectiveDate = Utilities.LocalDateAndTimeAsUtc(_effectivedate); + _page.ExpiryDate = Utilities.LocalDateAndTimeAsUtc(_expirydate); _page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable)); // appearance diff --git a/Oqtane.Client/Modules/Admin/Profiles/Edit.razor b/Oqtane.Client/Modules/Admin/Profiles/Edit.razor index 91fe98ec..6c820ac2 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Edit.razor @@ -34,7 +34,7 @@
- +
@@ -76,6 +76,12 @@
+
+ +
+ +
+
@@ -89,7 +95,7 @@
@SharedLocalizer["Cancel"] - @if (PageState.QueryString.ContainsKey("id")) + @if (PageState.QueryString.ContainsKey("id")) {

@@ -111,6 +117,7 @@ private string _defaultvalue = string.Empty; private string _options = string.Empty; private string _validation = string.Empty; + private string _autocomplete = string.Empty; private string _isrequired = "False"; private string _isprivate = "False"; private string createdby; @@ -142,6 +149,7 @@ _defaultvalue = profile.DefaultValue; _options = profile.Options; _validation = profile.Validation; + _autocomplete = profile.Autocomplete; _isrequired = profile.IsRequired.ToString(); _isprivate = profile.IsPrivate.ToString(); createdby = profile.CreatedBy; @@ -187,8 +195,10 @@ profile.DefaultValue = _defaultvalue; profile.Options = _options; profile.Validation = _validation; + profile.Autocomplete = _autocomplete; profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired)); profile.IsPrivate = (_isprivate == null ? false : Boolean.Parse(_isprivate)); + if (_profileid != -1) { profile = await ProfileService.UpdateProfileAsync(profile); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index c55eb2ba..01a6bba3 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -579,6 +579,11 @@ if (logofileid != -1) { site.LogoFileId = logofileid; + if (logofileid != _logofileid) + { + _logofileid = logofileid; + refresh = true; // needs to be refreshed on client + } } int? faviconFieldId = _faviconfilemanager.GetFileId(); if (faviconFieldId == -1) faviconFieldId = null; @@ -807,41 +812,55 @@ } } - private async Task SaveAlias() - { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { - if (!string.IsNullOrEmpty(_aliasname)) - { - var aliases = await AliasService.GetAliasesAsync(); - var alias = aliases.Where(item => item.Name == _aliasname).FirstOrDefault(); - bool unique = (alias == null || alias.AliasId == _aliasid); - if (unique) - { - if (_aliasid == 0) - { - alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; - await AliasService.AddAliasAsync(alias); - } - else - { - alias = _aliases.Single(item => item.AliasId == _aliasid); - alias.Name = _aliasname; - alias.IsDefault = bool.Parse(_defaultalias); - await AliasService.UpdateAliasAsync(alias); - } - } - else // duplicate alias - { - AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); - } - } - await GetAliases(); - _aliasid = -1; - _aliasname = ""; - StateHasChanged(); - } - } + private async Task SaveAlias() + { + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + if (!string.IsNullOrEmpty(_aliasname)) + { + var aliases = await AliasService.GetAliasesAsync(); + + int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); + if (protocolIndex != -1) + { + _aliasname = _aliasname.Substring(protocolIndex + 3); + } + + var alias = aliases.FirstOrDefault(item => item.Name == _aliasname); + + bool unique = (alias == null || alias.AliasId == _aliasid); + + if (unique) + { + if (_aliasid == 0) + { + alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; + await AliasService.AddAliasAsync(alias); + } + else + { + alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid); + if (alias != null) + { + alias.Name = _aliasname; + alias.IsDefault = bool.Parse(_defaultalias); + await AliasService.UpdateAliasAsync(alias); + } + } + + await GetAliases(); + _aliasid = -1; + _aliasname = ""; + StateHasChanged(); + } + else // Duplicate alias + { + AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); + await ScrollToPageTop(); + } + } + } + } private async Task CancelAlias() { diff --git a/Oqtane.Client/Modules/Admin/Sites/Index.razor b/Oqtane.Client/Modules/Admin/Sites/Index.razor index 20a652ee..58f281a5 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Index.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Index.razor @@ -48,11 +48,25 @@ else private void Edit(string name) { - NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name + "/admin/site", true); + if (PageState.Alias.Name == name) + { + NavigationManager.NavigateTo("/admin/site"); + } + else + { + NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name + "/admin/site", true); + } } private void Browse(string name) { - NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name, true); + if (PageState.Alias.Name == name) + { + NavigationManager.NavigateTo("/"); + } + else + { + NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name, true); + } } } diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index be4c362f..0f5dad7b 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -103,44 +103,91 @@
- @if (!string.IsNullOrEmpty(p.Options)) + @if (!string.IsNullOrEmpty(p.Options)) { - + @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - + @if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option)) + { + + } + else + { + + } } - else + + } + else + { + + + } } else { @if (p.Rows == 1) { - @if (p.IsRequired) + if (!string.IsNullOrEmpty(p.Autocomplete)) { - + @if (p.IsRequired) + { + + } + else + { + + } } else { - + @if (p.IsRequired) + { + + } + else + { + + } } } else { - @if (p.IsRequired) + if (!string.IsNullOrEmpty(p.Autocomplete)) { - + @if (p.IsRequired) + { + + } + else + { + + } } else { - + @if (p.IsRequired) + { + + } + else + { + + } } } } @@ -360,10 +407,11 @@ 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); + var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + _ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); + _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; await LoadNotificationsAsync(); @@ -561,12 +609,12 @@ await NotificationService.DeleteNotificationAsync(Notification.NotificationId); } await logger.LogInformation("Notification Deleted {Notification}", Notification); - } + } await logger.LogInformation("Notifications Permanently Deleted"); await LoadNotificationsAsync(); ModuleInstance.HideProgressIndicator(); - StateHasChanged(); + StateHasChanged(); } catch (Exception ex) { @@ -574,21 +622,19 @@ AddModuleMessage(ex.Message, MessageType.Error); ModuleInstance.HideProgressIndicator(); } - } private void TogglePassword() - { - if (_passwordtype == "password") - { - _passwordtype = "text"; - _togglepassword = SharedLocalizer["HidePassword"]; - } - else - { - _passwordtype = "password"; - _togglepassword = SharedLocalizer["ShowPassword"]; - } - } - + { + if (_passwordtype == "password") + { + _passwordtype = "text"; + _togglepassword = SharedLocalizer["HidePassword"]; + } + else + { + _passwordtype = "password"; + _togglepassword = SharedLocalizer["ShowPassword"]; + } + } } diff --git a/Oqtane.Client/Modules/Admin/Users/Roles.razor b/Oqtane.Client/Modules/Admin/Users/Roles.razor index 8766c46a..8d65480a 100644 --- a/Oqtane.Client/Modules/Admin/Users/Roles.razor +++ b/Oqtane.Client/Modules/Admin/Users/Roles.razor @@ -34,13 +34,13 @@ else
- +
- +
@@ -60,8 +60,8 @@ else @context.Role.Name - @context.EffectiveDate - @context.ExpiryDate + @Utilities.UtcAsLocalDate(context.EffectiveDate) + @Utilities.UtcAsLocalDate(context.ExpiryDate) @@ -75,8 +75,8 @@ else private string name = string.Empty; private List roles; private int roleid = -1; - private DateTime? effectivedate = null; - private DateTime? expirydate = null; + private DateTime? _effectivedate = null; + private DateTime? _expirydate = null; private List userroles; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; @@ -92,7 +92,7 @@ else if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true); - roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated); + roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated); } else { @@ -113,6 +113,7 @@ else try { userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, userid); + } catch (Exception ex) { @@ -127,11 +128,16 @@ else { if (roleid != -1) { + if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) + { + AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); + return; + } var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault(); if (userrole != null) { - userrole.EffectiveDate = effectivedate; - userrole.ExpiryDate = expirydate; + userrole.EffectiveDate = _effectivedate; + userrole.ExpiryDate = _expirydate; await UserRoleService.UpdateUserRoleAsync(userrole); } else @@ -139,15 +145,15 @@ else userrole = new UserRole(); userrole.UserId = userid; userrole.RoleId = roleid; - userrole.EffectiveDate = effectivedate; - userrole.ExpiryDate = expirydate; + userrole.EffectiveDate = Utilities.UtcAsLocalDate(_effectivedate); + userrole.ExpiryDate = Utilities.UtcAsLocalDate(_expirydate); await UserRoleService.AddUserRoleAsync(userrole); } await logger.LogInformation("User Assigned To Role {UserRole}", userrole); AddModuleMessage(Localizer["Success.User.AssignRole"], MessageType.Success); await GetUserRoles(); - StateHasChanged(); + StateHasChanged(); } else { diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 1bbe4581..a6a863aa 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -4,6 +4,7 @@ @inject IFolderService FolderService @inject IFileService FileService @inject ISettingService SettingService +@inject IUserService UserService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -344,7 +345,7 @@ _message = string.Empty; var interop = new Interop(JSRuntime); var uploads = await interop.GetFiles(_fileinputid); - + if (uploads.Length > 0) { string restricted = ""; @@ -370,7 +371,12 @@ // upload the files var posturl = Utilities.TenantUrl(PageState.Alias, "/api/file/upload"); var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString(); - await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken); + var jwt = ""; + if (PageState.Runtime == Shared.Runtime.Hybrid) + { + jwt = await UserService.GetTokenAsync(); + } + await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt); // uploading is asynchronous so we need to poll to determine if uploads are completed var success = true; diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index ad7980d1..61672446 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -162,27 +162,31 @@ _richhtml = Content; _rawhtml = Content; _originalrawhtml = _rawhtml; // preserve for comparison later + _originalrichhtml = ""; } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); - var interop = new RichTextEditorInterop(JSRuntime); - - if (firstRender) + if (AllowRichText) { - await interop.CreateEditor( - _editorElement, - _toolBar, - ReadOnly, - Placeholder, - Theme, - DebugLevel); + var interop = new RichTextEditorInterop(JSRuntime); + + if (firstRender) + { + await interop.CreateEditor( + _editorElement, + _toolBar, + ReadOnly, + Placeholder, + Theme, + DebugLevel); + } await interop.LoadEditorContent(_editorElement, _richhtml); - if (AllowRichText) + if (string.IsNullOrEmpty(_originalrichhtml)) { // 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); @@ -256,7 +260,8 @@ { var interop = new RichTextEditorInterop(JSRuntime); await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name)); - _richfilemanager = false; + _richhtml = await interop.GetHtml(_editorElement); + _richfilemanager = false; } else { diff --git a/Oqtane.Client/Modules/HtmlText/Index.razor b/Oqtane.Client/Modules/HtmlText/Index.razor index 77f7b95d..91efe2bb 100644 --- a/Oqtane.Client/Modules/HtmlText/Index.razor +++ b/Oqtane.Client/Modules/HtmlText/Index.razor @@ -15,17 +15,20 @@ } @code { - private string content = ""; + private string content = ""; - protected override async Task OnParametersSetAsync() - { - try + protected override async Task OnParametersSetAsync() + { + try { - var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); - if (htmltext != null) + if (ShouldRender()) { - content = htmltext.Content; - content = Utilities.FormatContent(content, PageState.Alias, "render"); + var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); + if (htmltext != null) + { + content = htmltext.Content; + content = Utilities.FormatContent(content, PageState.Alias, "render"); + } } } catch (Exception ex) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index f5c0bef7..05281844 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -113,6 +113,11 @@ namespace Oqtane.Modules } } + protected override bool ShouldRender() + { + return PageState?.RenderId == ModuleState?.RenderId; + } + // path method public string ModulePath() @@ -266,6 +271,7 @@ namespace Oqtane.Modules public void AddModuleMessage(string message, MessageType type, string position) { + ClearModuleMessage(); ModuleInstance.AddModuleMessage(message, type, position); } diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 880f5e1b..c825a93e 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net8.0 Exe Debug;Release - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -21,10 +21,10 @@ - - - - + + + + diff --git a/Oqtane.Client/Resources/Modules/Admin/Jobs/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Jobs/Edit.resx index facc630b..672f5dc9 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Jobs/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Jobs/Edit.resx @@ -192,4 +192,7 @@ Execute Once + + Start Date cannot be after End Date. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx index cac13d97..94942a50 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx @@ -130,16 +130,16 @@ Please Note That The Module Creator Is Only Intended To Be Used In A Development Environment - You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template + You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same Or Contain The Word "Oqtane" ) And Choose A Template You Must Provide A Valid Description (ie. No Punctuation) - Enter the name of the organization who is developing this module. It should not contain spaces or punctuation. + Enter the name of the organization who is developing this module. It should not contain spaces or punctuation or contain the word "oqtane". - Enter a name for this module. It should not contain spaces or punctuation. + Enter a name for this module. It should not contain spaces or punctuation or contain the word "oqtane". Enter a short description for the module @@ -171,4 +171,4 @@ The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Module. - \ No newline at end of file + diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx index 5b7849ec..722fa2f1 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx @@ -165,4 +165,16 @@ Pane: + + The date that this module is active + + + Effective Date: + + + The date that this module expires + + + Expiry Date: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx index 62972b72..9ebed89a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx @@ -255,4 +255,16 @@ Theme Settings + + The date that this page is active + + + Effective Date: + + + The date that this page expires + + + Expiry Date: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx index 984e6461..478e7616 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx @@ -285,4 +285,16 @@ Please Note That Overriding The Default Site Theme With An Unrelated Page Theme May Result In Compatibility Issues For Your Site + + The date that this page is active + + + Effective Date: + + + The date that this page expires + + + Expiry Date: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx index 973f8719..79a2f0f4 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx @@ -1,4 +1,4 @@ - + Exe - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -31,7 +31,7 @@ 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 - 5.0.1 + 5.0.2 1 14.2 @@ -65,11 +65,11 @@ - - + + - + diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css index 5da25ceb..c1d0b44b 100644 --- a/Oqtane.Maui/wwwroot/css/app.css +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -213,3 +213,18 @@ app { right: 0.75rem; top: 0.5rem; } + +/* Oqtane Control Styles */ + +/* Pager */ +.app-pager-pointer { + cursor: pointer; +} + +.app-sort-th { + cursor: pointer; +} + +.app-fas { + margin-left: 5px; +} diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 8a677952..8305f766 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -27,14 +27,8 @@ Oqtane.Interop = { document.title = title; } }, - includeMeta: function (id, attribute, name, content, key) { - var meta; - if (id !== "" && key === "id") { - meta = document.getElementById(id); - } - else { - meta = document.querySelector("meta[" + attribute + "=\"" + CSS.escape(name) + "\"]"); - } + includeMeta: function (id, attribute, name, content) { + var meta = document.querySelector("meta[" + attribute + "=\"" + CSS.escape(name) + "\"]"); if (meta === null) { meta = document.createElement("meta"); meta.setAttribute(attribute, name); @@ -119,13 +113,26 @@ Oqtane.Interop = { this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); } }, - includeScript: function (id, src, integrity, crossorigin, content, location) { - var script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); + includeScript: function (id, src, integrity, crossorigin, type, content, location) { + var script; + if (src !== "") { + script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); + } + else { + script = document.getElementById(id); + } + if (script !== null) { + script.remove(); + script = null; + } if (script === null) { script = document.createElement("script"); if (id !== "") { script.id = id; } + if (type !== "") { + script.type = type; + } if (src !== "") { script.src = src; if (integrity !== "") { @@ -141,43 +148,22 @@ Oqtane.Interop = { script.async = false; this.addScript(script, location) .then(() => { - console.log(src + ' loaded'); + if (src !== "") { + console.log(src + ' loaded'); + } + else { + console.log(id + ' loaded'); + } }) .catch(() => { - console.error(src + ' failed'); + if (src !== "") { + console.error(src + ' failed'); + } + else { + console.error(id + ' failed'); + } }); } - else { - if (script.id !== id) { - script.setAttribute('id', id); - } - if (src !== "") { - if (script.src !== this.getAbsoluteUrl(src)) { - script.removeAttribute('integrity'); - script.removeAttribute('crossorigin'); - script.src = src; - } - if (integrity !== "") { - if (script.integrity !== integrity) { - script.setAttribute('integrity', integrity); - } - } else { - script.removeAttribute('integrity'); - } - if (crossorigin !== "") { - if (script.crossOrigin !== crossorigin) { - script.setAttribute('crossorigin', crossorigin); - } - } else { - script.removeAttribute('crossorigin'); - } - } - else { - if (script.innerHTML !== content) { - script.innerHTML = content; - } - } - } }, addScript: function (script, location) { if (location === 'head') { @@ -229,6 +215,10 @@ Oqtane.Interop = { if (path === scripts[s].href && scripts[s].es6module === true) { element.type = "module"; } + if (path === scripts[s].href && scripts[s].location === 'body') { + document.body.appendChild(element); + return false; // return false to bypass default DOM insertion mechanism + } } } }) @@ -289,19 +279,21 @@ Oqtane.Interop = { var fileinput = document.getElementById(id); if (fileinput !== null) { for (var i = 0; i < fileinput.files.length; i++) { - files.push(fileinput.files[i].name); + files.push(fileinput.files[i].name + ":" + fileinput.files[i].size); } } return files; }, - uploadFiles: function (posturl, folder, id, antiforgerytoken) { - var fileinput = document.getElementById(id + 'FileInput'); + uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { + var fileinput = document.getElementById('FileInput_' + id); var files = fileinput.files; - var progressinfo = document.getElementById(id + 'ProgressInfo'); - var progressbar = document.getElementById(id + 'ProgressBar'); + var progressinfo = document.getElementById('ProgressInfo_' + id); + var progressbar = document.getElementById('ProgressBar_' + id); - progressinfo.setAttribute("style", "display: inline;"); - progressbar.setAttribute("style", "width: 200px; display: inline;"); + if (progressinfo !== null && progressbar !== null) { + progressinfo.setAttribute("style", "display: inline;"); + progressbar.setAttribute("style", "width: 100%; display: inline;"); + } for (var i = 0; i < files.length; i++) { var FileChunk = []; @@ -331,22 +323,34 @@ Oqtane.Interop = { data.append('formfile', Chunk, FileName); var request = new XMLHttpRequest(); request.open('POST', posturl, true); + if (jwt !== "") { + request.setRequestHeader('Authorization', 'Bearer ' + jwt); + request.withCredentials = true; + } request.upload.onloadstart = function (e) { - progressinfo.innerHTML = file.name + ' 0%'; - progressbar.value = 0; + if (progressinfo !== null && progressbar !== null) { + progressinfo.innerHTML = file.name + ' 0%'; + progressbar.value = 0; + } }; request.upload.onprogress = function (e) { - var percent = Math.ceil((e.loaded / e.total) * 100); - progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%'; - progressbar.value = (percent / 100); + if (progressinfo !== null && progressbar !== null) { + var percent = Math.ceil((e.loaded / e.total) * 100); + progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%'; + progressbar.value = (percent / 100); + } }; request.upload.onloadend = function (e) { - progressinfo.innerHTML = file.name + ' 100%'; - progressbar.value = 1; + if (progressinfo !== null && progressbar !== null) { + progressinfo.innerHTML = file.name + ' 100%'; + progressbar.value = 1; + } }; - request.upload.onerror = function () { - progressinfo.innerHTML = file.name + ' Error: ' + xhr.status; - progressbar.value = 0; + request.upload.onerror = function() { + if (progressinfo !== null && progressbar !== null) { + progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; + progressbar.value = 0; + } }; request.send(data); } @@ -356,10 +360,15 @@ Oqtane.Interop = { } } }, - refreshBrowser: function (reload, wait) { - setInterval(function () { - window.location.reload(reload); - }, wait * 1000); + refreshBrowser: function (verify, wait) { + async function attemptReload (verify) { + if (verify) { + await fetch(''); + } + window.location.reload(); + } + attemptReload(verify); + setInterval(attemptReload, wait * 1000); }, redirectBrowser: function (url, wait) { setInterval(function () { diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 4c346c87..06250592 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 5.0.1 + 5.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,14 +12,13 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 icon.png oqtane - \ No newline at end of file diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 3b0d65b4..671ba15a 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 5.0.1 + 5.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - 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 + https://github.com/oqtane/oqtane.framework/releases/download/v5.0.2/Oqtane.Framework.5.0.2.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 65c3d5c4..1b87c609 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 8c6be30a..294d5c5c 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 5.0.1 + 5.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,14 +12,13 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 icon.png oqtane - \ No newline at end of file diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index aee8ee24..e16b7bd9 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 634cf373..3f986170 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.1.Install.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.2.Install.zip" -Force \ No newline at end of file diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index b9538e69..f1fe650e 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.1.Upgrade.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.2.Upgrade.zip" -Force \ No newline at end of file diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 65cd3fec..7dc5cfb6 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -20,6 +20,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Formats.Png; using System.Net.Http; +using Microsoft.AspNetCore.Cors; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -288,7 +289,8 @@ namespace Oqtane.Controllers folder = _folders.GetFolder(FolderId); } - var _UploadableFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue ?? Constants.UploadableFiles) ?? Constants.UploadableFiles; + var _UploadableFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue; + _UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles; if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList)) { @@ -357,17 +359,18 @@ namespace Oqtane.Controllers } // POST api//upload + [EnableCors(Constants.MauiCorsPolicy)] [HttpPost("upload")] public async Task UploadFile(string folder, IFormFile formfile) { - if (formfile.Length <= 0) + if (formfile == null || formfile.Length <= 0) { return; } // Get the UploadableFiles extensions - string uploadfilesSetting = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue; - string _UploadableFiles = uploadfilesSetting ?? Constants.UploadableFiles; + string _UploadableFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue; + _UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles; // ensure filename is valid string token = ".part_"; @@ -612,7 +615,9 @@ namespace Oqtane.Controllers { var file = _files.GetFile(id); - var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles; + var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue; + _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; + if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) { if (_ImageFiles.Split(',').Contains(file.Extension.ToLower())) @@ -779,7 +784,9 @@ 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; + + var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue; + _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; int size = 0; var folder = _folders.GetFolder(folderid, false); diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index f8172db1..8acde463 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -75,6 +75,8 @@ namespace Oqtane.Controllers module.Pane = pagemodule.Pane; module.Order = pagemodule.Order; module.ContainerType = pagemodule.ContainerType; + module.EffectiveDate = pagemodule.EffectiveDate; + module.ExpiryDate = pagemodule.ExpiryDate; module.ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == module.ModuleDefinitionName)); @@ -169,7 +171,7 @@ namespace Oqtane.Controllers { if (!pageModules.Exists(item => item.ModuleId == module.ModuleId && item.PageId == page.PageId) && !page.Path.StartsWith("admin/")) { - _pageModules.AddPageModule(new PageModule { PageId = page.PageId, ModuleId = pageModule.ModuleId, Title = pageModule.Title, Pane = pageModule.Pane, Order = pageModule.Order, ContainerType = pageModule.ContainerType }); + _pageModules.AddPageModule(new PageModule { PageId = page.PageId, ModuleId = pageModule.ModuleId, Title = pageModule.Title, Pane = pageModule.Pane, Order = pageModule.Order, ContainerType = pageModule.ContainerType, EffectiveDate = pageModule.EffectiveDate, ExpiryDate = pageModule.ExpiryDate }); } } } diff --git a/Oqtane.Server/Controllers/SiteController.cs b/Oqtane.Server/Controllers/SiteController.cs index 4d1b7184..df0384c1 100644 --- a/Oqtane.Server/Controllers/SiteController.cs +++ b/Oqtane.Server/Controllers/SiteController.cs @@ -13,6 +13,8 @@ using System.Globalization; using Microsoft.Extensions.Caching.Memory; using Oqtane.Extensions; using System; +using Oqtane.UI; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Oqtane.Controllers { @@ -97,7 +99,7 @@ namespace Oqtane.Controllers site.Pages = new List(); foreach (Page page in _pages.GetPages(site.SiteId)) { - if (!page.IsDeleted && _userPermissions.IsAuthorized(User, PermissionNames.View, page.PermissionList)) + if (!page.IsDeleted && _userPermissions.IsAuthorized(User, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || _userPermissions.IsAuthorized(User, PermissionNames.Edit, page.PermissionList))) { page.Settings = settings.Where(item => item.EntityId == page.PageId) .Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(User, PermissionNames.Edit, page.PermissionList)) @@ -105,46 +107,53 @@ namespace Oqtane.Controllers site.Pages.Add(page); } } + site.Pages = GetPagesHierarchy(site.Pages); // modules List moduledefinitions = _moduleDefinitions.GetModuleDefinitions(site.SiteId).ToList(); settings = _settings.GetSettings(EntityNames.Module).ToList(); site.Modules = new List(); - foreach (PageModule pagemodule in _pageModules.GetPageModules(site.SiteId)) + foreach (PageModule pagemodule in _pageModules.GetPageModules(site.SiteId).Where(pm => !pm.IsDeleted && _userPermissions.IsAuthorized(User, PermissionNames.View, pm.Module.PermissionList))) { - if (!pagemodule.IsDeleted && _userPermissions.IsAuthorized(User, PermissionNames.View, pagemodule.Module.PermissionList)) + if(Utilities.IsPageModuleVisible(pagemodule.EffectiveDate, pagemodule.ExpiryDate) || _userPermissions.IsAuthorized(User, PermissionNames.Edit, pagemodule.Module.PermissionList)) { - Module module = new Module(); - module.SiteId = pagemodule.Module.SiteId; - module.ModuleDefinitionName = pagemodule.Module.ModuleDefinitionName; - module.AllPages = pagemodule.Module.AllPages; - module.PermissionList = pagemodule.Module.PermissionList; - module.CreatedBy = pagemodule.Module.CreatedBy; - module.CreatedOn = pagemodule.Module.CreatedOn; - module.ModifiedBy = pagemodule.Module.ModifiedBy; - module.ModifiedOn = pagemodule.Module.ModifiedOn; - module.DeletedBy = pagemodule.DeletedBy; - module.DeletedOn = pagemodule.DeletedOn; - module.IsDeleted = pagemodule.IsDeleted; + Module module = new Module + { + SiteId = pagemodule.Module.SiteId, + ModuleDefinitionName = pagemodule.Module.ModuleDefinitionName, + AllPages = pagemodule.Module.AllPages, + PermissionList = pagemodule.Module.PermissionList, + CreatedBy = pagemodule.Module.CreatedBy, + CreatedOn = pagemodule.Module.CreatedOn, + ModifiedBy = pagemodule.Module.ModifiedBy, + ModifiedOn = pagemodule.Module.ModifiedOn, + DeletedBy = pagemodule.DeletedBy, + DeletedOn = pagemodule.DeletedOn, + IsDeleted = pagemodule.IsDeleted, - module.PageModuleId = pagemodule.PageModuleId; - module.ModuleId = pagemodule.ModuleId; - module.PageId = pagemodule.PageId; - module.Title = pagemodule.Title; - module.Pane = pagemodule.Pane; - module.Order = pagemodule.Order; - module.ContainerType = pagemodule.ContainerType; + PageModuleId = pagemodule.PageModuleId, + ModuleId = pagemodule.ModuleId, + PageId = pagemodule.PageId, + Title = pagemodule.Title, + Pane = pagemodule.Pane, + Order = pagemodule.Order, + ContainerType = pagemodule.ContainerType, + EffectiveDate = pagemodule.EffectiveDate, + ExpiryDate = pagemodule.ExpiryDate, - module.ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == module.ModuleDefinitionName)); + ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)), - module.Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId) + Settings = settings + .Where(item => item.EntityId == pagemodule.ModuleId) .Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(User, PermissionNames.Edit, pagemodule.Module.PermissionList)) - .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue) + }; site.Modules.Add(module); } } + site.Modules = site.Modules.OrderBy(item => item.PageId).ThenBy(item => item.Pane).ThenBy(item => item.Order).ToList(); // languages diff --git a/Oqtane.Server/Databases/DatabaseBase.cs b/Oqtane.Server/Databases/DatabaseBase.cs index a9b91eaf..2342874f 100644 --- a/Oqtane.Server/Databases/DatabaseBase.cs +++ b/Oqtane.Server/Databases/DatabaseBase.cs @@ -66,6 +66,11 @@ namespace Oqtane.Databases return name; } + public virtual string RewriteName(string name, bool isQuery) + { + return name; + } + public virtual string RewriteValue(string value, string type) { return value; diff --git a/Oqtane.Server/Databases/Interfaces/IDatabase.cs b/Oqtane.Server/Databases/Interfaces/IDatabase.cs index 303fbc0f..3c68cf03 100644 --- a/Oqtane.Server/Databases/Interfaces/IDatabase.cs +++ b/Oqtane.Server/Databases/Interfaces/IDatabase.cs @@ -28,6 +28,8 @@ namespace Oqtane.Databases.Interfaces public string RewriteName(string name); + public string RewriteName(string name, bool isQuery); + public string RewriteValue(string value, string type); public void UpdateIdentityStoreTableNames(ModelBuilder builder); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 68a002c7..604270c1 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -205,6 +205,10 @@ namespace Oqtane.Extensions { email = item[emailClaimType].ToString().ToLower(); } + else + { + id = ""; // if email is not valid we will assume id is not valid + } } } if (!string.IsNullOrEmpty(id)) @@ -290,6 +294,10 @@ namespace Oqtane.Extensions { email = context.Principal.FindFirstValue(emailClaimType); } + else + { + id = ""; // if email is not valid we will assume id is not valid + } } // validate user @@ -610,23 +618,27 @@ namespace Oqtane.Extensions private static bool EmailValid(string email, string domainfilter) { - if (!string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains(".")) + if (!string.IsNullOrEmpty(email)) { - var domains = domainfilter.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var domain in domains) + if (email.Contains("@") && email.Contains(".")) { - if (domain.StartsWith("!")) + var domains = domainfilter.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var domain in domains) { - if (email.ToLower().Contains(domain.Substring(1))) return false; - } - else - { - if (!email.ToLower().Contains(domain)) return false; + if (domain.StartsWith("!")) + { + if (email.ToLower().Contains(domain.Substring(1))) return false; + } + else + { + if (!email.ToLower().Contains(domain)) return false; + } } + return true; } - return true; - } - return false; + return false; + } + return (string.IsNullOrEmpty(domainfilter)); // email is optional unless domain filter is specified } } } diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 01138129..d05524ca 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Loader; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; @@ -29,16 +26,14 @@ namespace Oqtane.Infrastructure { private readonly IConfigManager _config; private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly IWebHostEnvironment _environment; private readonly IMemoryCache _cache; private readonly IConfigManager _configManager; private readonly ILogger _filelogger; - public DatabaseManager(IConfigManager config, IServiceScopeFactory serviceScopeFactory, IWebHostEnvironment environment, IMemoryCache cache, IConfigManager configManager, ILogger filelogger) + public DatabaseManager(IConfigManager config, IServiceScopeFactory serviceScopeFactory, IMemoryCache cache, IConfigManager configManager, ILogger filelogger) { _config = config; _serviceScopeFactory = serviceScopeFactory; - _environment = environment; _cache = cache; _configManager = configManager; _filelogger = filelogger; @@ -216,19 +211,27 @@ namespace Oqtane.Infrastructure // get database type var type = Type.GetType(databaseType); - // create database object from type - var database = Activator.CreateInstance(type) as IDatabase; - - // create data directory if does not exist - var dataDirectory = AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString(); - if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory ?? String.Empty); - - var dbOptions = new DbContextOptionsBuilder().UseOqtaneDatabase(database, NormalizeConnectionString(install.ConnectionString)).Options; - using (var dbc = new DbContext(dbOptions)) + if (type != null) { - // create empty database if it does not exist - dbc.Database.EnsureCreated(); - result.Success = true; + // create database object from type + var database = Activator.CreateInstance(type) as IDatabase; + + // create data directory if does not exist + var dataDirectory = AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString(); + if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory ?? String.Empty); + + var dbOptions = new DbContextOptionsBuilder().UseOqtaneDatabase(database, NormalizeConnectionString(install.ConnectionString)).Options; + using (var dbc = new DbContext(dbOptions)) + { + // create empty database if it does not exist + dbc.Database.EnsureCreated(); + result.Success = true; + } + } + else + { + result.Message = $"The Database Provider {databaseType} Does Not Exist. If This Is A Development Environment Please Ensure You Have Performed A Full Compilation Of All Projects In The Oqtane Solution Prior To Running The Application."; + _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } } catch (Exception ex) diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index 0d26600b..1307b57f 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -137,16 +137,20 @@ namespace Oqtane.Infrastructure // register assembly if (Path.GetExtension(filename) == ".dll") { - // if package version was not installed previously - if (!File.Exists(Path.Combine(sourceFolder, name + ".log"))) + // do not register licensing assemblies + if (!Path.GetFileName(filename).StartsWith("Oqtane.Licensing.")) { - if (assemblies.ContainsKey(Path.GetFileName(filename))) + // if package version was not installed previously + if (!File.Exists(Path.Combine(sourceFolder, name + ".log"))) { - assemblies[Path.GetFileName(filename)] += 1; - } - else - { - assemblies.Add(Path.GetFileName(filename), 1); + if (assemblies.ContainsKey(Path.GetFileName(filename))) + { + assemblies[Path.GetFileName(filename)] += 1; + } + else + { + assemblies.Add(Path.GetFileName(filename), 1); + } } } } @@ -255,22 +259,26 @@ namespace Oqtane.Infrastructure // delete assets if (Path.GetExtension(filepath) == ".dll") { - // use assembly log to determine if assembly is used in other packages - if (assemblies.ContainsKey(Path.GetFileName(filepath))) + // do not remove licensing assemblies + if (!Path.GetFileName(filepath).StartsWith("Oqtane.Licensing.")) { - if (assemblies[Path.GetFileName(filepath)] == 1) + // use assembly log to determine if assembly is used in other packages + if (assemblies.ContainsKey(Path.GetFileName(filepath))) + { + if (assemblies[Path.GetFileName(filepath)] == 1) + { + DeleteFile(filepath); + assemblies.Remove(Path.GetFileName(filepath)); + } + else + { + assemblies[Path.GetFileName(filepath)] -= 1; + } + } + else // does not exist in assembly log { 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 diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 6819277a..e572c5f0 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -68,7 +68,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, - Content = "

Copyright (c) 2018-2023 .NET Foundation

" + + Content = "

Copyright (c) 2018-2024 .NET Foundation

" + "

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

" + "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index 5bf10e0a..b97a3d60 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -50,6 +50,11 @@ namespace Oqtane.Migrations.EntityBuilders return ActiveDatabase.RewriteName(name); } + private string RewriteName(string name, bool isQuery) + { + return ActiveDatabase.RewriteName(name, isQuery); + } + private string RewriteValue(string value, string type) { return ActiveDatabase.RewriteValue(value, type); @@ -394,7 +399,7 @@ namespace Oqtane.Migrations.EntityBuilders public void UpdateColumn(string columnName, string value, string type, string condition) { - var updateSql = $"UPDATE {RewriteSqlEntityTableName(EntityTableName)} SET {RewriteName(columnName)} = {RewriteValue(value, type)} "; + var updateSql = $"UPDATE {RewriteSqlEntityTableName(EntityTableName)} SET {RewriteName(columnName, true)} = {RewriteValue(value, type)} "; if (!string.IsNullOrEmpty(condition)) { updateSql += $"WHERE {condition}"; diff --git a/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs b/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs index 8379c3e3..87fb34cc 100644 --- a/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs +++ b/Oqtane.Server/Migrations/Framework/MultiDatabaseMigration.cs @@ -14,7 +14,12 @@ namespace Oqtane.Migrations protected string RewriteName(string name) { - return ActiveDatabase.RewriteName(name); + return ActiveDatabase.RewriteName(name, false); + } + + protected string RewriteName(string name, bool isQuery) + { + return ActiveDatabase.RewriteName(name, isQuery); } } } diff --git a/Oqtane.Server/Migrations/Tenant/05010001_AddPageEffectiveExpiryDate.cs b/Oqtane.Server/Migrations/Tenant/05010001_AddPageEffectiveExpiryDate.cs new file mode 100644 index 00000000..bed22cd2 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/05010001_AddPageEffectiveExpiryDate.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +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.01.00.01")] + public class AddPageEffectiveExpiryDate : MultiDatabaseMigration + { + public AddPageEffectiveExpiryDate(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase);; + pageEntityBuilder.AddDateTimeColumn("EffectiveDate", true); + pageEntityBuilder.AddDateTimeColumn("ExpiryDate", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase); + pageEntityBuilder.DropColumn("EffectiveDate"); + pageEntityBuilder.DropColumn("ExpiryDate"); + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/05010002_AddPageModuleEffectiveExpiryDate.cs b/Oqtane.Server/Migrations/Tenant/05010002_AddPageModuleEffectiveExpiryDate.cs new file mode 100644 index 00000000..f0b9147d --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/05010002_AddPageModuleEffectiveExpiryDate.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +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.01.00.02")] + public class AddPageModuleEffectiveExpiryDate : MultiDatabaseMigration + { + public AddPageModuleEffectiveExpiryDate(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase);; + pageModuleEntityBuilder.AddDateTimeColumn("EffectiveDate", true); + pageModuleEntityBuilder.AddDateTimeColumn("ExpiryDate", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase); + pageModuleEntityBuilder.DropColumn("EffectiveDate"); + pageModuleEntityBuilder.DropColumn("ExpiryDate"); + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/05010003_AddProfileAutocomplete.cs b/Oqtane.Server/Migrations/Tenant/05010003_AddProfileAutocomplete.cs new file mode 100644 index 00000000..6ec00156 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/05010003_AddProfileAutocomplete.cs @@ -0,0 +1,28 @@ +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.01.00.03")] + public class AddProfileAutocomplete : MultiDatabaseMigration + { + public AddProfileAutocomplete(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var profileEntityBuilder = new ProfileEntityBuilder(migrationBuilder, ActiveDatabase); + profileEntityBuilder.AddStringColumn("Autocomplete", 50, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 3b7d540a..70f59532 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -33,21 +33,20 @@
- - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 935525ee..075646e4 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -228,7 +228,7 @@ namespace Oqtane.Repository // build list of scripts for site if (moduledefinition.Resources != null) { - foreach (var resource in moduledefinition.Resources.Where(item => item.Level == ResourceLevel.Site)) + foreach (var resource in moduledefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site)) { if (!serverState.Scripts.Contains(resource)) { diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index a650f7d3..86d14d86 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -200,7 +200,7 @@ namespace Oqtane.Repository // build list of scripts for site if (theme.Resources != null) { - foreach (var resource in theme.Resources.Where(item => item.Level == ResourceLevel.Site)) + foreach (var resource in theme.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site)) { if (!serverState.Scripts.Contains(resource)) { diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index da191dcc..e777f4ed 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -131,6 +131,16 @@ namespace Oqtane .WithSiteIdentity() .WithSiteAuthentication(); + services.AddCors(options => + { + options.AddPolicy(Constants.MauiCorsPolicy, + policy => + { + policy.WithOrigins("https://0.0.0.0", "http://0.0.0.0", "app://0.0.0.0") + .AllowAnyHeader().AllowCredentials(); + }); + }); + services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); @@ -180,6 +190,7 @@ namespace Oqtane app.UseJwtAuthorization(); app.UseBlazorFrameworkFiles(); app.UseRouting(); + app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 32e6c1a1..48bbf770 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,10 +13,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Repository/[Module]Context.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Repository/[Module]Context.cs index a921ca0f..2cce3945 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Repository/[Module]Context.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Repository/[Module]Context.cs @@ -15,5 +15,12 @@ namespace [Owner].Module.[Module].Repository { // ContextBase handles multi-tenant database connections } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity().ToTable(ActiveDatabase.RewriteName("[Owner][Module]")); + } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 7062f5ac..7411d973 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index 727b74dd..3ab97068 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 7daf41c1..8305f766 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -284,7 +284,7 @@ Oqtane.Interop = { } return files; }, - uploadFiles: function (posturl, folder, id, antiforgerytoken) { + uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { var fileinput = document.getElementById('FileInput_' + id); var files = fileinput.files; var progressinfo = document.getElementById('ProgressInfo_' + id); @@ -323,6 +323,10 @@ Oqtane.Interop = { data.append('formfile', Chunk, FileName); var request = new XMLHttpRequest(); request.open('POST', posturl, true); + if (jwt !== "") { + request.setRequestHeader('Authorization', 'Bearer ' + jwt); + request.withCredentials = true; + } request.upload.onloadstart = function (e) { if (progressinfo !== null && progressbar !== null) { progressinfo.innerHTML = file.name + ' 0%'; @@ -344,7 +348,7 @@ Oqtane.Interop = { }; request.upload.onerror = function() { if (progressinfo !== null && progressbar !== null) { - progressinfo.innerHTML = file.name + ' Error: ' + xhr.status; + progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; progressbar.value = 0; } }; diff --git a/Oqtane.Shared/Documentation/readme.md b/Oqtane.Shared/Documentation/readme.md index cd55726a..ba715490 100644 --- a/Oqtane.Shared/Documentation/readme.md +++ b/Oqtane.Shared/Documentation/readme.md @@ -2,6 +2,6 @@ This folder contains special attributes for the API Code Generator. -The idea is that only items marked with special attributes are valide public APIs, and only these will be documented in the public docs +The idea is that only items marked with special attributes are valid public APIs, and only these will be documented in the public docs -As of 2020, all APIs are documented, and only these marked as `[PrivateApi]` will be excluded. In future, we may reverse this to only document things marked as `[PublicApi]`. \ No newline at end of file +As of 2020, all APIs are documented, and only these marked as `[PrivateApi]` will be excluded. In future, we may reverse this to only document things marked as `[PublicApi]`. diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index 07c2a507..c0d37b3a 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -75,16 +75,24 @@ namespace Oqtane.Models [NotMapped] public string ContainerType { get; set; } + [NotMapped] + public DateTime? EffectiveDate { get; set; } + + [NotMapped] + public DateTime? ExpiryDate { get; set; } + #endregion #region SiteRouter properties - + [NotMapped] public string ModuleType { get; set; } [NotMapped] public int PaneModuleIndex { get; set; } [NotMapped] public int PaneModuleCount { get; set; } + [NotMapped] + public Guid RenderId { get; set; } #endregion diff --git a/Oqtane.Shared/Models/Page.cs b/Oqtane.Shared/Models/Page.cs index c4c74a9a..a1b6d370 100644 --- a/Oqtane.Shared/Models/Page.cs +++ b/Oqtane.Shared/Models/Page.cs @@ -82,6 +82,14 @@ namespace Oqtane.Models public bool IsNavigation { get; set; } public bool IsClickable { get; set; } public int? UserId { get; set; } + /// + /// Start of when this assignment is valid. See also + /// + public DateTime? EffectiveDate { get; set; } + /// + /// End of when this assignment is valid. See also + /// + public DateTime? ExpiryDate { get; set; } public bool IsPersonalizable { get; set; } #region IDeletable Properties diff --git a/Oqtane.Shared/Models/PageModule.cs b/Oqtane.Shared/Models/PageModule.cs index e8ff50e5..e3f0adb9 100644 --- a/Oqtane.Shared/Models/PageModule.cs +++ b/Oqtane.Shared/Models/PageModule.cs @@ -41,7 +41,14 @@ namespace Oqtane.Models /// Reference to a Razor Container which wraps this module instance. /// public string ContainerType { get; set; } - + /// + /// Start of when this assignment is valid. See also + /// + public DateTime? EffectiveDate { get; set; } + /// + /// End of when this assignment is valid. See also + /// + public DateTime? ExpiryDate { get; set; } #region IDeletable Properties public string DeletedBy { get; set; } diff --git a/Oqtane.Shared/Models/Profile.cs b/Oqtane.Shared/Models/Profile.cs index 336cd262..f4db036d 100644 --- a/Oqtane.Shared/Models/Profile.cs +++ b/Oqtane.Shared/Models/Profile.cs @@ -78,5 +78,11 @@ namespace Oqtane.Models /// Optional number of rows (textarea) /// public int Rows { get; set; } + + /// + /// Autocomplete setting for the property. + /// If set, enable autocomplete for the corresponding input field. + /// + public string Autocomplete { get; set; } } } diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 265cc578..d2b161f9 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -19,11 +19,11 @@ - - + + - + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index d0b44394..efef37e3 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -1,14 +1,11 @@ using System; -using Oqtane.Models; -using System.Collections.Generic; -using System.Runtime.InteropServices; namespace Oqtane.Shared { public class Constants { - 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 static readonly string Version = "5.0.2"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; @@ -80,6 +77,8 @@ namespace Oqtane.Shared public static readonly string MauiUserAgent = "MAUI"; public static readonly string MauiAliasPath = "Alias-Path"; + public const string MauiCorsPolicy = "MauiCorsPolicy"; // must be a constant to be used with an attribute + public static readonly string VisitorCookiePrefix = "APP_VISITOR_"; // Obsolete constants diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 82b32796..c00d8cc8 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -572,7 +572,55 @@ namespace Oqtane.Shared return (localDateTime?.Date, localTime); } + public static bool IsPageModuleVisible(DateTime? effectiveDate, DateTime? expiryDate) + { + DateTime currentUtcTime = DateTime.UtcNow; + if (effectiveDate.HasValue && expiryDate.HasValue) + { + return currentUtcTime >= effectiveDate.Value && currentUtcTime <= expiryDate.Value; + } + else if (effectiveDate.HasValue) + { + return currentUtcTime >= effectiveDate.Value; + } + else if (expiryDate.HasValue) + { + // Include equality check here + return currentUtcTime <= expiryDate.Value; + } + else + { + return true; + } + } + public static bool ValidateEffectiveExpiryDates(DateTime? effectiveDate, DateTime? expiryDate) + { + // Treat DateTime.MinValue as null + effectiveDate ??= DateTime.MinValue; + expiryDate ??= DateTime.MinValue; + + // Check if both effectiveDate and expiryDate have values + if (effectiveDate != DateTime.MinValue && expiryDate != DateTime.MinValue) + { + return effectiveDate <= expiryDate; + } + // Check if only effectiveDate has a value + else if (effectiveDate != DateTime.MinValue) + { + return true; + } + // Check if only expiryDate has a value + else if (expiryDate != DateTime.MinValue) + { + return true; + } + // If neither effectiveDate nor expiryDate has a value, consider the page/module visible + else + { + return true; + } + } [Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)] public static string ContentUrl(Alias alias, int fileId) { diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index bc4faa12..616bc2b9 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net8.0 Exe - 5.0.1 + 5.0.2 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.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/README.md b/README.md index 7d6922eb..0ba4b8f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Latest Release -[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. +[5.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1) was released on Dec 21, 2023 and is a major release targeted at .NET 8. This release includes 67 pull requests by 6 different contributors, pushing the total number of project commits all-time to over 4400. 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) @@ -19,10 +19,18 @@ Please note that this project is owned by the .NET Foundation and is governed by **Using Version 5:** - Install **[.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**. - + - 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. +- Clone the Oqtane dev branch source code to your local system. + +- Open the **Oqtane.sln** solution file. + +- **Important:** Build the solution. + +- Make sure you specify Oqtane.Server as the Startup Project + +- Run the application. **Installing an official release:** @@ -39,6 +47,11 @@ Please note that this project is owned by the .NET Foundation and is governed by - If you are getting started with Oqtane, a [series of videos](https://www.youtube.com/watch?v=JPfUZPlRRCE&list=PLYhXmd7yV0elLNLfQwZBUlM7ZSMYPTZ_f) are available which explain how to install the product, interact with the user interface, and develop custom modules. +# Oqtane Marketplace + +Explore and enhance your Oqtane experience by visiting the Oqtane Marketplace. Discover a variety of modules, themes, and extensions contributed by the community. [Visit Oqtane Marketplace](https://www.oqtane.net) + + # Documentation There is a separate [Documentation repository](https://github.com/oqtane/oqtane.docs) which contains a variety of types of documentation for Oqtane, including API documentation that is auto generated using Docfx. The contents of the repository is published to Githib Pages and is available at [https://docs.oqtane.org](https://docs.oqtane.org/) @@ -53,6 +66,9 @@ Backlog (TBD) 5.1.0 (Q1 2024) - [ ] Full Stack Blazor (Static Server-Side Rendering) +[5.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1) (Dec 21, 2023) +- [x] Stabilization improvements + [5.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0) (Nov 16, 2023) - [x] Migration to .NET 8