Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to import users #3267

Merged
merged 1 commit into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Oqtane.Client/Modules/Admin/Users/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ else
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />
</div>
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Text="Import Users" Class="btn btn-secondary" Action="Users" Security="SecurityAccessLevel.Admin" />
</div>
<div class="col-sm-4">
<input class="form-control" @bind="@_search" />
</div>
Expand Down Expand Up @@ -54,7 +55,7 @@ else
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
</TabPanel>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
Expand Down Expand Up @@ -360,7 +361,7 @@ else
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabPanel>
</TabStrip>
}

Expand Down
55 changes: 55 additions & 0 deletions Oqtane.Client/Modules/Admin/Users/Users.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@namespace Oqtane.Modules.Admin.Users
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer

<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userfile" HelpText="Select or upload a CSV file containing user information. The CSV file must be in the Template format specified." ResourceKey="UserFile">User File:</Label>
<div class="col-sm-9">
<FileManager Id="userfile" @ref="_filemanager" Filter="csv" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportUsers">@Localizer["Import"]</button>&nbsp;
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>&nbsp;
<a class="btn btn-info" href="/users.csv" target="_new">@Localizer["Template"]</a>

@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);
}
}
}
144 changes: 144 additions & 0 deletions Oqtane.Client/Resources/Modules/Admin/Users/Users.resx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

Version 2.0

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>

There are any number of "resheader" rows that contain simple
name/value pairs.

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="UserFile.HelpText" xml:space="preserve">
<value>Select or upload a CSV file containing user information. The CSV file must be in the Template format specified.</value>
</data>
<data name="UserFile.Text" xml:space="preserve">
<value>User File:</value>
</data>
<data name="Error.Import" xml:space="preserve">
<value>Error Importing Users</value>
</data>
<data name="Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Message.Import.Failure" xml:space="preserve">
<value>User Import Failed</value>
</data>
<data name="Message.Import.Success" xml:space="preserve">
<value>Users Imported Successfully</value>
</data>
<data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Specify A User File For Import</value>
</data>
<data name="Template" xml:space="preserve">
<value>Template</value>
</data>
</root>
7 changes: 7 additions & 0 deletions Oqtane.Client/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,12 @@ public interface IUserService
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <returns></returns>
Task<string> GetPasswordRequirementsAsync(int siteId);

/// <summary>
/// Bulk import of users
/// </summary>
/// <param name="fileId">ID of a <see cref="File"/></param>
/// <returns></returns>
Task<bool> ImportUsersAsync(int siteId, int fileId);
}
}
8 changes: 8 additions & 0 deletions Oqtane.Client/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
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
{
Expand Down Expand Up @@ -123,5 +126,10 @@ public async Task<string> GetPasswordRequirementsAsync(int siteId)
// format requirements
return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
}

public async Task<bool> ImportUsersAsync(int siteId, int fileId)
{
return await PostJsonAsync<bool>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}", true);
}
}
}
21 changes: 20 additions & 1 deletion Oqtane.Server/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ public class UserController : Controller
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;
Expand All @@ -39,6 +40,7 @@ public UserController(IUserRepository users, ITenantManager tenantManager, IUser
_userPermissions = userPermissions;
_settings = settings;
_jwtManager = jwtManager;
_files = files;
_logger = logger;
}

Expand Down Expand Up @@ -369,5 +371,22 @@ public Dictionary<string, string> PasswordRequirements(int siteid)

return requirements;
}

// POST api/<controller>/import?siteid=x&fileid=y
[HttpPost("import")]
[Authorize(Roles = RoleNames.Admin)]
public async Task<bool> 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;
}
}
}
}
1 change: 1 addition & 0 deletions Oqtane.Server/Managers/Interfaces/IUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ public interface IUserManager
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<bool> ValidatePassword(string password);
Task<bool> ImportUsers(int siteId, int fileId);
}
}
Loading