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) : "") |
-
+
-
+
}
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