From eb5a0dc1c9f121e83ab78bd80fbf03696892e8a1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 08:25:50 -0400 Subject: [PATCH 1/7] improve filename validation in module content export --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 9 +++++++-- .../Services/Interfaces/IModuleService.cs | 4 ++-- Oqtane.Client/Services/ModuleService.cs | 4 ++-- Oqtane.Server/Controllers/FileController.cs | 1 - Oqtane.Server/Controllers/ModuleController.cs | 16 ++++++---------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index d72ec689..10831a56 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -50,6 +50,11 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; + protected override void OnInitialized() + { + _filename = Utilities.GetFriendlyUrl(ModuleState.Title); + } + private async Task ExportText() { try @@ -71,8 +76,8 @@ var folderid = _filemanager.GetFolderId(); if (folderid != -1 && !string.IsNullOrEmpty(_filename)) { - var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); - if (result.Success) + var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); + if (fileid != -1) { AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); } diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index a2334a20..ea6beab3 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -67,7 +67,7 @@ namespace Oqtane.Services /// /// /// - /// success/failure - Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); + /// file id + Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index 68d1e4fd..ac093bed 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -51,9 +51,9 @@ namespace Oqtane.Services return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } - public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) { - return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); } } } diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 7ead95d7..f0b72f22 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -22,7 +22,6 @@ using Microsoft.AspNetCore.Cors; using System.IO.Compression; using Oqtane.Services; using Microsoft.Extensions.Primitives; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; // ReSharper disable StringIndexOfIsCultureSpecific.1 diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 012f2e11..61f69f33 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -10,9 +10,6 @@ using Oqtane.Repository; using Oqtane.Security; using System.Net; using System.IO; -using System; -using static System.Net.WebRequestMethods; -using System.Net.Http; namespace Oqtane.Controllers { @@ -259,9 +256,9 @@ namespace Oqtane.Controllers // POST api//export?moduleid=x&pageid=y&folderid=z&filename=a [HttpPost("export")] [Authorize(Roles = RoleNames.Registered)] - public Result Export(int moduleid, int pageid, int folderid, string filename) + public int Export(int moduleid, int pageid, int folderid, string filename) { - var result = new Result(false); + var fileid = -1; var module = _modules.GetModule(moduleid); if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) @@ -278,7 +275,7 @@ namespace Oqtane.Controllers } // create json file - filename = Path.GetFileNameWithoutExtension(filename) + ".json"; + filename = Utilities.GetFriendlyUrl(Path.GetFileNameWithoutExtension(filename)) + ".json"; string filepath = Path.Combine(folderPath, filename); if (System.IO.File.Exists(filepath)) { @@ -298,9 +295,7 @@ namespace Oqtane.Controllers file.Size = (int)new FileInfo(filepath).Length; _files.UpdateFile(file); } - - result.Success = true; - result.Message = filename; + fileid = file.FileId; _logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid); } @@ -309,7 +304,8 @@ namespace Oqtane.Controllers _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } - return result; + + return fileid; } // POST api//import?moduleid=x&pageid=y From bbd6f13f369d0f3871c31ace90c5bafc62db42cd Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 09:09:07 -0400 Subject: [PATCH 2/7] fix initialization issue related to time zones --- .../Modules/Admin/Register/Index.razor | 143 +++++++++--------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 13920441..712ba186 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -8,88 +8,92 @@ @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService -@if (PageState.Site.AllowRegistration) +@if (_initialized) { - if (!_userCreated) + @if (PageState.Site.AllowRegistration) { - if (PageState.User != null) + if (!_userCreated) { - - } - else - { - -
-
-
- -
- + if (PageState.User != null) + { + + } + else + { + + +
+
+ +
+ +
-
-
- -
-
- - +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- - - @if (_allowsitelogin) - {
+ + + @if (_allowsitelogin) + { +
-
- @Localizer["Login"] - } - +
+ @Localizer["Login"] + } + + } } - } -} -else -{ - + } + else + { + + } } @code { + private bool _initialized = false; private List _timezones; private string _passwordrequirements; private string _username = string.Empty; @@ -113,6 +117,7 @@ else _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _timezones = await TimeZoneService.GetTimeZonesAsync(); _timezoneid = PageState.Site.TimeZoneId; + _initialized = true; } protected override void OnParametersSet() From 1f05d12ef55c06edbfd4000191309ad9cf3575d0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 09:39:57 -0400 Subject: [PATCH 3/7] fix spelling mistake --- Oqtane.Server/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 7a0f3bfd..37648c4f 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -131,7 +131,7 @@ namespace Oqtane.Controllers filtered.TwoFactorCode = ""; filtered.SecurityStamp = ""; - // include private properties if authenticated user is accessing their own user account os is an administrator + // include private properties if authenticated user is accessing their own user account or is an administrator if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) { filtered.Email = user.Email; From ff6a810ad589126cceee37fa255af9b4f4b22a58 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:13:03 -0400 Subject: [PATCH 4/7] Fix #4789 - allow user email verification to be managed by administrator --- Oqtane.Client/Modules/Admin/Login/Index.razor | 2 +- Oqtane.Client/Modules/Admin/Users/Edit.razor | 28 +++++++++---- .../Resources/Modules/Admin/Login/Index.resx | 4 +- .../Resources/Modules/Admin/Users/Edit.resx | 6 +++ Oqtane.Server/Controllers/UserController.cs | 10 ++++- Oqtane.Server/Managers/UserManager.cs | 39 ++++++++++++------- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f10c0203..f0a057d0 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -144,7 +144,7 @@ else user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); if (user != null) { - await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username); AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); } else diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 549059e4..c6401a47 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -18,13 +18,13 @@
- +
- +
@@ -33,7 +33,7 @@
- +
@@ -42,13 +42,22 @@
- +
- + +
+ +
+
+
+
@@ -68,7 +77,7 @@ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
- +
- +
@@ -167,6 +176,7 @@ private string _togglepassword = string.Empty; private string _confirm = string.Empty; private string _email = string.Empty; + private string _confirmed = string.Empty; private string _displayname = string.Empty; private string _timezoneid = string.Empty; private string _isdeleted; @@ -204,6 +214,7 @@ { _username = user.Username; _email = user.Email; + _confirmed = user.EmailConfirmed.ToString(); _displayname = user.DisplayName; _timezoneid = PageState.User.TimeZoneId; _isdeleted = user.IsDeleted.ToString(); @@ -255,6 +266,7 @@ user.Username = _username; user.Password = _password; user.Email = _email; + user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 0b1a8780..f1fb0c8a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -121,10 +121,10 @@ Forgot Password - User Account Verified Successfully. You Can Now Login With Your Username And Password Below. + User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password. - User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions. + User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions. User Account Linked Successfully. You Can Now Login With Your External Login Below. diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index df4ccc95..ff38ec85 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -216,4 +216,10 @@ The user's time zone + + Confirmed? + + + Indicates if the user's email is verified + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 37648c4f..7789a2d3 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -140,6 +140,7 @@ namespace Oqtane.Controllers filtered.LastLoginOn = user.LastLoginOn; filtered.LastIPAddress = user.LastIPAddress; filtered.TwoFactorRequired = user.TwoFactorRequired; + filtered.EmailConfirmed = user.EmailConfirmed; filtered.Roles = user.Roles; filtered.CreatedBy = user.CreatedBy; filtered.CreatedOn = user.CreatedOn; @@ -200,10 +201,15 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null + var existing = _userManager.GetUser(user.UserId, user.SiteId); + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { - user.EmailConfirmed = User.IsInRole(RoleNames.Admin); + // only administrators can update the email confirmation + if (!User.IsInRole(RoleNames.Admin)) + { + user.EmailConfirmed = existing.EmailConfirmed; + } user = await _userManager.UpdateUser(user); } else diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 84679d23..5e1e6e64 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -65,7 +65,12 @@ namespace Oqtane.Managers { user.SiteId = siteid; user.Roles = GetUserRoles(user.UserId, user.SiteId); - user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp; + var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult(); + if (identityuser != null) + { + user.SecurityStamp = identityuser.SecurityStamp; + user.EmailConfirmed = identityuser.EmailConfirmed; + } user.Settings = _settings.GetSettings(EntityNames.User, user.UserId) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); } @@ -245,22 +250,30 @@ namespace Oqtane.Managers { identityuser.Email = user.Email; await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated - - // if email address changed and it is not confirmed, verification is required for new email address - if (!user.EmailConfirmed) - { - 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); - } } if (user.EmailConfirmed) { - var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + if (!identityuser.EmailConfirmed) + { + var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + + string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password."; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); + } + } + else + { + identityuser.EmailConfirmed = false; + await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated + + 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); } user = _users.UpdateUser(user); From 5bde40ec2bd042f27f41c26deb7a1afa4dc4c074 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:46:53 -0400 Subject: [PATCH 5/7] improve messaging --- Oqtane.Client/Resources/Modules/Admin/Login/Index.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index f1fb0c8a..c60c0716 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -133,7 +133,7 @@ External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions. - Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. + Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification. Please Provide All Required Fields From fe9f1897341868e997e3b2ed9491f1f5dfe7f57f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:53:04 -0400 Subject: [PATCH 6/7] improve comment --- Oqtane.Shared/Models/Folder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs index da875219..47177cdb 100644 --- a/Oqtane.Shared/Models/Folder.cs +++ b/Oqtane.Shared/Models/Folder.cs @@ -43,7 +43,7 @@ namespace Oqtane.Models public string Path { get; set; } /// - /// Sorting order of the folder + /// Sorting order of the folder ** not used as folders are sorted in alphabetical order ** /// public int Order { get; set; } From a4370829526a261f8a1cefaff6608a46ea0ed87b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 12:11:03 -0400 Subject: [PATCH 7/7] use consistent authorization method --- Oqtane.Server/Controllers/UserController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 7789a2d3..859bd50c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -205,8 +205,8 @@ namespace Oqtane.Controllers if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { - // only administrators can update the email confirmation - if (!User.IsInRole(RoleNames.Admin)) + // only authorized users can update the email confirmation + if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin)) { user.EmailConfirmed = existing.EmailConfirmed; }