From 16477052e2fec5e51c78507d019a78446a556fba Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 21 Jan 2025 12:21:27 -0500 Subject: [PATCH] fix #4965 - improve user/site management --- Oqtane.Client/Modules/Admin/Roles/Users.razor | 33 +++---- Oqtane.Client/Modules/Admin/Users/Edit.razor | 51 +++++++--- Oqtane.Client/Modules/Admin/Users/Index.razor | 28 ++++-- Oqtane.Client/Modules/Admin/Users/Roles.razor | 24 +++-- .../Resources/Modules/Admin/Users/Edit.resx | 9 ++ .../Resources/Modules/Admin/Users/Index.resx | 6 ++ Oqtane.Client/Resources/SharedResources.resx | 2 +- Oqtane.Server/Controllers/UserController.cs | 2 +- ...taneSiteAuthenticationBuilderExtensions.cs | 95 ++++++++++--------- Oqtane.Server/Managers/UserManager.cs | 42 ++++---- 10 files changed, 187 insertions(+), 105 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Roles/Users.razor b/Oqtane.Client/Modules/Admin/Roles/Users.razor index d0eb0aac..5f8a6e99 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Users.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Users.razor @@ -58,7 +58,7 @@ else @context.EffectiveDate @context.ExpiryDate - + @@ -180,27 +180,28 @@ else private async Task DeleteUserRole(int UserRoleId) { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) + try { - try + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else { await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); - AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); - await GetUserRoles(); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); - AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); } + AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); + await GetUserRoles(); + StateHasChanged(); } - else + catch (Exception ex) { - AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); + AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); } } } diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index a58af948..e4aedaa0 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -51,15 +51,18 @@ -
- -
- + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+ +
+ +
-
+ }
@@ -127,8 +130,11 @@ @SharedLocalizer["Cancel"] -
-
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") + { + + } +

} @@ -226,8 +232,10 @@ user.Password = _password; user.Email = email; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; - - user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + } user = await UserService.UpdateUserAsync(user); if (user != null) @@ -259,6 +267,25 @@ } } + private async Task DeleteUser() + { + try + { + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) + { + var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); + await logger.LogInformation("User Permanently Deleted {User}", user); + NavigationManager.NavigateTo(NavigateUrl()); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); + } + } + private bool ValidateProfiles() { foreach (Profile profile in profiles) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index c6114047..8af3d381 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -35,7 +35,7 @@ else - + @@ -611,19 +611,31 @@ else { try { - var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); - if (user != null) + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); - await logger.LogInformation("User Deleted {User}", UserRole.User); - await LoadUsersAsync(true); - StateHasChanged(); + var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); + if (user != null) + { + user.IsDeleted = true; + await UserService.UpdateUserAsync(user); + await logger.LogInformation("User Soft Deleted {User}", user); + } } + else + { + var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId); + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success); + await LoadUsersAsync(true); + StateHasChanged(); } catch (Exception ex) { await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); - AddModuleMessage(ex.Message, MessageType.Error); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Roles.razor b/Oqtane.Client/Modules/Admin/Users/Roles.razor index 8d65480a..5ce12b62 100644 --- a/Oqtane.Client/Modules/Admin/Users/Roles.razor +++ b/Oqtane.Client/Modules/Admin/Users/Roles.razor @@ -53,17 +53,17 @@ else

- @Localizer["Roles"] - @Localizer["Effective"] - @Localizer["Expiry"] -   + @Localizer["Roles"] + @Localizer["Effective"] + @Localizer["Expiry"] +  
@context.Role.Name @Utilities.UtcAsLocalDate(context.EffectiveDate) @Utilities.UtcAsLocalDate(context.ExpiryDate) - + @@ -171,8 +171,18 @@ else { try { - await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else + { + await UserRoleService.DeleteUserRoleAsync(UserRoleId); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); + } AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success); await GetUserRoles(); StateHasChanged(); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index f56d3798..29aed594 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -195,4 +195,13 @@ Last Login: + + Delete User + + + Delete + + + Are You Sure You Wish To Permanently Delete This User? + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 021788e8..34884fb5 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -501,4 +501,10 @@ Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie. + + User Deleted Successfully + + + Error Deleting User + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index 8c5ae010..b32eb955 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -427,7 +427,7 @@ At Least One Uppercase Letter - Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site. + Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site. {0} Is Not Valid diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 6d146758..92aef5c5 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -217,7 +217,7 @@ namespace Oqtane.Controllers // DELETE api//5?siteid=x [HttpDelete("{id}")] - [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")] + [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")] public async Task Delete(int id, string siteid) { User user = _users.GetUser(id, false); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 0a7b1094..0e58e4b6 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -524,11 +524,6 @@ namespace Oqtane.Extensions // manage user if (user != null) { - // update user - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); - _users.UpdateUser(user); - // manage roles var _userRoles = httpContext.RequestServices.GetRequiredService(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); @@ -588,64 +583,78 @@ namespace Oqtane.Extensions } } - // create claims identity - identityuser = await _identityUserManager.FindByNameAsync(user.Username); - user.SecurityStamp = identityuser.SecurityStamp; - identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); - identity.Label = ExternalLoginStatus.Success; - - // user profile claims - if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) + var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered); + if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate)) { - var _settings = httpContext.RequestServices.GetRequiredService(); - var _profiles = httpContext.RequestServices.GetRequiredService(); - var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); - foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) + // update user + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); + _users.UpdateUser(user); + + // create claims identity + identityuser = await _identityUserManager.FindByNameAsync(user.Username); + user.SecurityStamp = identityuser.SecurityStamp; + identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); + identity.Label = ExternalLoginStatus.Success; + + // user profile claims + if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) { - if (mapping.Contains(":")) + var _settings = httpContext.RequestServices.GetRequiredService(); + var _profiles = httpContext.RequestServices.GetRequiredService(); + var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); + foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) { - var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); - if (claim != null) + if (mapping.Contains(":")) { - var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); - if (profile != null) + var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); + if (claim != null) { - if (!string.IsNullOrEmpty(claim.Value)) + var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); + if (profile != null) { - var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); - if (setting != null) + if (!string.IsNullOrEmpty(claim.Value)) { - setting.SettingValue = claim.Value; - _settings.UpdateSetting(setting); - } - else - { - setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; - _settings.AddSetting(setting); + var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); + if (setting != null) + { + setting.SettingValue = claim.Value; + _settings.UpdateSetting(setting); + } + else + { + setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; + _settings.AddSetting(setting); + } } } + else + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); } } - else - { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); - } } + + var _syncManager = httpContext.RequestServices.GetRequiredService(); + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); + } + else + { + identity.Label = ExternalLoginStatus.AccessDenied; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External User Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId); } - - var _syncManager = httpContext.RequestServices.GetRequiredService(); - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); } } else // claims invalid diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index c1a91f5d..694bfd03 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -363,28 +363,36 @@ namespace Oqtane.Managers } else { - user = _users.GetUser(identityuser.UserName); - if (user != null) + if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) { - if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) + user = GetUser(identityuser.UserName, alias.SiteId); + if (user != null) { - user.IsAuthenticated = true; - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = LastIPAddress; - _users.UpdateUser(user); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); - - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - if (setCookie) + // ensure user is registered for site + if (user.Roles.Contains(RoleNames.Registered)) { - await _identitySignInManager.SignInAsync(identityuser, isPersistent); + user.IsAuthenticated = true; + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = LastIPAddress; + _users.UpdateUser(user); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); + + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId); } } - else - { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); - } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); } } }