From 8e5e79a799f6ee27d199b8d666d3733402619ae7 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 20 Sep 2023 10:43:49 -0400 Subject: [PATCH] add ability to import users --- Oqtane.Client/Modules/Admin/Users/Index.razor | 9 +- Oqtane.Client/Modules/Admin/Users/Users.razor | 55 ++++++ .../Resources/Modules/Admin/Users/Users.resx | 144 ++++++++++++++++ .../Services/Interfaces/IUserService.cs | 7 + Oqtane.Client/Services/UserService.cs | 8 + Oqtane.Server/Controllers/UserController.cs | 21 ++- .../Managers/Interfaces/IUserManager.cs | 1 + Oqtane.Server/Managers/UserManager.cs | 156 +++++++++++++++++- .../Interfaces/IUserRoleRepository.cs | 2 + .../Repository/UserRoleRepository.cs | 23 +++ Oqtane.Server/wwwroot/users.csv | 1 + 11 files changed, 421 insertions(+), 6 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/Users/Users.razor create mode 100644 Oqtane.Client/Resources/Modules/Admin/Users/Users.resx create mode 100644 Oqtane.Server/wwwroot/users.csv diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 6957e3e6..d88b8a25 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -20,8 +20,9 @@ else
- -
+   + +
@@ -54,7 +55,7 @@ else @((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "") - +
@@ -360,7 +361,7 @@ else

-
+ } diff --git a/Oqtane.Client/Modules/Admin/Users/Users.razor b/Oqtane.Client/Modules/Admin/Users/Users.razor new file mode 100644 index 00000000..9f6b3401 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Users/Users.razor @@ -0,0 +1,55 @@ +@namespace Oqtane.Modules.Admin.Users +@inherits ModuleBase +@inject NavigationManager NavigationManager +@inject IUserService UserService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+ +
+ +
+
+
+
+  +@SharedLocalizer["Cancel"]  +@Localizer["Template"] + +@code { + private FileManager _filemanager; + + public override string Title => "Import Users"; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + private async Task ImportUsers() + { + try + { + var fileid = _filemanager.GetFileId(); + if (fileid != -1) + { + if (await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid)) + { + AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success); + } + else + { + AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.Import.Validation"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Importing Users {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Import"], MessageType.Error); + } + } +} diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx new file mode 100644 index 00000000..b18df840 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Select or upload a CSV file containing user information. The CSV file must be in the Template format specified. + + + User File: + + + Error Importing Users + + + Import + + + User Import Failed + + + Users Imported Successfully + + + You Must Specify A User File For Import + + + Template + + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index f2a7a83b..04661e8f 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -142,5 +142,12 @@ namespace Oqtane.Services /// ID of a /// Task GetPasswordRequirementsAsync(int siteId); + + /// + /// Bulk import of users + /// + /// ID of a + /// + Task ImportUsersAsync(int siteId, int fileId); } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 033b585d..770c4eb1 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -6,6 +6,9 @@ using Oqtane.Documentation; using System.Net; using System.Collections.Generic; using Microsoft.Extensions.Localization; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Oqtane.Modules.Admin.Roles; +using System.Xml.Linq; namespace Oqtane.Services { @@ -123,5 +126,10 @@ namespace Oqtane.Services // format requirements return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement); } + + public async Task ImportUsersAsync(int siteId, int fileId) + { + return await PostJsonAsync($"{Apiurl}/import?siteid={siteId}&fileid={fileId}", true); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index e57635cb..3788817c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -28,9 +28,10 @@ namespace Oqtane.Controllers private readonly IUserPermissions _userPermissions; private readonly ISettingRepository _settings; private readonly IJwtManager _jwtManager; + private readonly IFileRepository _files; private readonly ILogManager _logger; - public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, ISettingRepository settings, IJwtManager jwtManager, ILogManager logger) + public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, ISettingRepository settings, IJwtManager jwtManager, IFileRepository files, ILogManager logger) { _users = users; _tenantManager = tenantManager; @@ -39,6 +40,7 @@ namespace Oqtane.Controllers _userPermissions = userPermissions; _settings = settings; _jwtManager = jwtManager; + _files = files; _logger = logger; } @@ -369,5 +371,22 @@ namespace Oqtane.Controllers return requirements; } + + // POST api//import?siteid=x&fileid=y + [HttpPost("import")] + [Authorize(Roles = RoleNames.Admin)] + public async Task Import(string siteid, string fileid) + { + if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId)) + { + return await _userManager.ImportUsers(SiteId, FileId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return false; + } + } } } diff --git a/Oqtane.Server/Managers/Interfaces/IUserManager.cs b/Oqtane.Server/Managers/Interfaces/IUserManager.cs index 82cf7090..7eb79e78 100644 --- a/Oqtane.Server/Managers/Interfaces/IUserManager.cs +++ b/Oqtane.Server/Managers/Interfaces/IUserManager.cs @@ -18,5 +18,6 @@ namespace Oqtane.Managers User VerifyTwoFactor(User user, string token); Task LinkExternalAccount(User user, string token, string type, string key, string name); Task ValidatePassword(string password); + Task ImportUsers(int siteId, int fileId); } } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 28e5b751..1a856607 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -16,24 +17,32 @@ namespace Oqtane.Managers public class UserManager : IUserManager { private readonly IUserRepository _users; + private readonly IRoleRepository _roles; private readonly IUserRoleRepository _userRoles; private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; private readonly ITenantManager _tenantManager; private readonly INotificationRepository _notifications; private readonly IFolderRepository _folders; + private readonly IFileRepository _files; + private readonly IProfileRepository _profiles; + private readonly ISettingRepository _settings; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; - public UserManager(IUserRepository users, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ILogManager logger) + public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IFileRepository files, IProfileRepository profiles, ISettingRepository settings, ISyncManager syncManager, ILogManager logger) { _users = users; + _roles = roles; _userRoles = userRoles; _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; _tenantManager = tenantManager; _notifications = notifications; _folders = folders; + _files = files; + _profiles = profiles; + _settings = settings; _syncManager = syncManager; _logger = logger; } @@ -95,6 +104,12 @@ namespace Oqtane.Managers IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); if (identityuser == null) { + if (string.IsNullOrEmpty(user.Password)) + { + // create random password ie. Jan-01-2023+12:00:00! + Random rnd = new Random(); + user.Password = DateTime.UtcNow.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47); + } identityuser = new IdentityUser(); identityuser.UserName = user.Username; identityuser.Email = user.Email; @@ -443,5 +458,144 @@ namespace Oqtane.Managers var result = await validator.ValidateAsync(_identityUserManager, null, password); return result.Succeeded; } + + public async Task ImportUsers(int siteId, int fileId) + { + var success = true; + int users = 0; + + var file = _files.GetFile(fileId); + if (file != null) + { + var path = _files.GetFilePath(file); + if (System.IO.File.Exists(path)) + { + var roles = _roles.GetRoles(siteId).ToList(); + var profiles = _profiles.GetProfiles(siteId).ToList(); + + try + { + string row; + using (var reader = new StreamReader(path)) + { + // get header row + row = reader.ReadLine(); + var header = row.Replace("\"", "").Split(','); + + row = reader.ReadLine(); + while (row != null) + { + var values = row.Replace("\"", "").Split(','); + + if (values.Length > 3) + { + // user + var user = _users.GetUser(values[1], values[0]); + if (user == null) + { + user = new User(); + user.SiteId = siteId; + user.Email = values[0]; + user.Username = (!string.IsNullOrEmpty(values[1])) ? values[1] : user.Email; + user.DisplayName = (!string.IsNullOrEmpty(values[2])) ? values[2] : user.Username; + user = await AddUser(user); + if (user == null) + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Creating User {Email}", values[0]); + success = false; + } + } + + if (user != null && !string.IsNullOrEmpty(values[3])) + { + // roles (comma delimited) + foreach (var rolename in values[3].Split(',')) + { + var role = roles.FirstOrDefault(item => item.Name == rolename); + if (role == null) + { + role = new Role(); + role.SiteId = siteId; + role.Name = rolename; + role.Description = rolename; + role = _roles.AddRole(role); + roles.Add(role); + } + if (role != null) + { + var userrole = _userRoles.GetUserRole(user.UserId, role.RoleId, false); + if (userrole == null) + { + userrole = new UserRole(); + userrole.UserId = user.UserId; + userrole.RoleId = role.RoleId; + _userRoles.AddUserRole(userrole); + } + } + } + } + + if (user != null && values.Length > 4) + { + var settings = _settings.GetSettings(EntityNames.User, user.UserId); + for (int index = 4; index < values.Length - 1; index++) + { + if (header.Length > index && !string.IsNullOrEmpty(values[index])) + { + var profile = profiles.FirstOrDefault(item => item.Name == header[index]); + if (profile != null) + { + var setting = settings.FirstOrDefault(item => item.SettingName == profile.Name); + if (setting == null) + { + setting = new Setting(); + setting.EntityName = EntityNames.User; + setting.EntityId = user.UserId; + setting.SettingName = profile.Name; + setting.SettingValue = values[index]; + _settings.AddSetting(setting); + } + else + { + if (setting.SettingValue != values[index]) + { + setting.SettingValue = values[index]; + _settings.UpdateSetting(setting); + } + } + } + } + } + } + + users++; + } + + row = reader.ReadLine(); + } + } + + _logger.Log(LogLevel.Information, this, LogFunction.Create, "{Users} Users Imported", users); + } + catch (Exception ex) + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "Error Importing User Import File {SiteId} {FileId}", siteId, fileId); + success = false; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Create,"User Import File Does Not Exist {Path}", path); + success = false; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import File Does Not Exist {SiteId} {FileId}", siteId, fileId); + success = false; + } + + return success; + } } } diff --git a/Oqtane.Server/Repository/Interfaces/IUserRoleRepository.cs b/Oqtane.Server/Repository/Interfaces/IUserRoleRepository.cs index 0c4a2290..7bed5f51 100644 --- a/Oqtane.Server/Repository/Interfaces/IUserRoleRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IUserRoleRepository.cs @@ -11,6 +11,8 @@ namespace Oqtane.Repository UserRole UpdateUserRole(UserRole userRole); UserRole GetUserRole(int userRoleId); UserRole GetUserRole(int userRoleId, bool tracking); + UserRole GetUserRole(int userId, int roleId); + UserRole GetUserRole(int userId, int roleId, bool tracking); void DeleteUserRole(int userRoleId); void DeleteUserRoles(int userId); } diff --git a/Oqtane.Server/Repository/UserRoleRepository.cs b/Oqtane.Server/Repository/UserRoleRepository.cs index 93acbe18..730009e1 100644 --- a/Oqtane.Server/Repository/UserRoleRepository.cs +++ b/Oqtane.Server/Repository/UserRoleRepository.cs @@ -78,6 +78,29 @@ namespace Oqtane.Repository } } + public UserRole GetUserRole(int userId, int roleId) + { + return GetUserRole(userId, roleId, true); + } + + public UserRole GetUserRole(int userId, int roleId, bool tracking) + { + if (tracking) + { + return _db.UserRole + .Include(item => item.Role) // eager load roles + .Include(item => item.User) // eager load users + .FirstOrDefault(item => item.UserId == userId && item.RoleId == roleId); + } + else + { + return _db.UserRole.AsNoTracking() + .Include(item => item.Role) // eager load roles + .Include(item => item.User) // eager load users + .FirstOrDefault(item => item.UserId == userId && item.RoleId == roleId); + } + } + public void DeleteUserRole(int userRoleId) { UserRole userRole = _db.UserRole.Find(userRoleId); diff --git a/Oqtane.Server/wwwroot/users.csv b/Oqtane.Server/wwwroot/users.csv new file mode 100644 index 00000000..ff6dcff3 --- /dev/null +++ b/Oqtane.Server/wwwroot/users.csv @@ -0,0 +1 @@ +Email,Username,DisplayName,Roles,FirstName,LastName,Street,City,Region,Country,PostalCode,Phone