2020-06-18 16:09:58 +00:00
#nullable enable
#pragma warning disable CA1801
using System ;
2020-06-17 19:08:58 +00:00
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
using System.Threading.Tasks ;
using Jellyfin.Api.Constants ;
2020-06-18 16:09:58 +00:00
using Jellyfin.Api.Helpers ;
using Jellyfin.Api.Models.UserDtos ;
2020-06-17 19:08:58 +00:00
using Jellyfin.Data.Enums ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Controller.Authentication ;
using MediaBrowser.Controller.Configuration ;
using MediaBrowser.Controller.Devices ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Net ;
using MediaBrowser.Controller.Session ;
2020-06-18 16:09:58 +00:00
using MediaBrowser.Model.Configuration ;
2020-06-17 19:08:58 +00:00
using MediaBrowser.Model.Dto ;
using MediaBrowser.Model.Users ;
using Microsoft.AspNetCore.Authorization ;
2020-06-18 16:09:58 +00:00
using Microsoft.AspNetCore.Http ;
2020-06-17 19:08:58 +00:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.ModelBinding ;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// User controller.
/// </summary>
[Route("/Users")]
public class UserController : BaseJellyfinApiController
{
private readonly IUserManager _userManager ;
private readonly ISessionManager _sessionManager ;
private readonly INetworkManager _networkManager ;
private readonly IDeviceManager _deviceManager ;
private readonly IAuthorizationContext _authContext ;
private readonly IServerConfigurationManager _config ;
/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public UserController (
IUserManager userManager ,
ISessionManager sessionManager ,
INetworkManager networkManager ,
IDeviceManager deviceManager ,
IAuthorizationContext authContext ,
IServerConfigurationManager config )
{
_userManager = userManager ;
_sessionManager = sessionManager ;
_networkManager = networkManager ;
_deviceManager = deviceManager ;
_authContext = authContext ;
_config = config ;
}
/// <summary>
/// Gets a list of users.
/// </summary>
/// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
/// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
/// <param name="isGuest">Optional filter by IsGuest=true or false.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">Users returned.</response>
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
2020-06-17 19:08:58 +00:00
[HttpGet]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-06-17 19:08:58 +00:00
public ActionResult < IEnumerable < UserDto > > GetUsers (
[FromQuery] bool? isHidden ,
[FromQuery] bool? isDisabled ,
[FromQuery] bool? isGuest )
{
2020-06-18 16:09:58 +00:00
var users = Get ( isHidden , isDisabled , false , false ) ;
return Ok ( users ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Gets a list of publicly visible users for display on a login screen.
/// </summary>
2020-06-18 16:09:58 +00:00
/// <response code="200">Public users returned.</response>
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
2020-06-17 19:08:58 +00:00
[HttpGet("Public")]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-06-17 19:08:58 +00:00
public ActionResult < IEnumerable < UserDto > > GetPublicUsers ( )
{
// If the startup wizard hasn't been completed then just return all users
if ( ! _config . Configuration . IsStartupWizardCompleted )
{
2020-06-18 16:09:58 +00:00
return Ok ( GetUsers ( false , false , false ) . Value ) ;
2020-06-17 19:08:58 +00:00
}
2020-06-18 16:09:58 +00:00
return Ok ( Get ( false , false , true , true ) ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Gets a user by Id.
/// </summary>
/// <param name="id">The user id.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">User returned.</response>
/// <response code="404">User not found.</response>
/// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
2020-06-17 19:08:58 +00:00
[HttpGet("{id}")]
// TODO: authorize escapeParentalControl
2020-06-18 16:09:58 +00:00
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-06-17 19:08:58 +00:00
public ActionResult < UserDto > GetUserById ( [ FromRoute ] Guid id )
{
var user = _userManager . GetUserById ( id ) ;
if ( user = = null )
{
2020-06-18 16:09:58 +00:00
return NotFound ( "User not found" ) ;
2020-06-17 19:08:58 +00:00
}
var result = _userManager . GetUserDto ( user , HttpContext . Connection . RemoteIpAddress . ToString ( ) ) ;
return Ok ( result ) ;
}
/// <summary>
/// Deletes a user.
/// </summary>
/// <param name="id">The user id.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">User deleted.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
2020-06-17 19:08:58 +00:00
[HttpDelete("{id}")]
[Authorize(Policy = Policies.RequiresElevation)]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-06-17 19:08:58 +00:00
public ActionResult DeleteUser ( [ FromRoute ] Guid id )
{
var user = _userManager . GetUserById ( id ) ;
if ( user = = null )
{
2020-06-18 16:09:58 +00:00
return NotFound ( "User not found" ) ;
2020-06-17 19:08:58 +00:00
}
_sessionManager . RevokeUserTokens ( user . Id , null ) ;
_userManager . DeleteUser ( user ) ;
return NoContent ( ) ;
}
/// <summary>
/// Authenticates a user.
/// </summary>
/// <param name="id">The user id.</param>
2020-06-18 16:09:58 +00:00
/// <param name="pw">The password as plain text.</param>
/// <param name="password">The password sha1-hash.</param>
/// <response code="200">User authenticated.</response>
/// <response code="403">Sha1-hashed password only is not allowed.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("{id}/Authenticate")]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-06-17 19:08:58 +00:00
public async Task < ActionResult < AuthenticationResult > > AuthenticateUser (
[FromRoute, Required] Guid id ,
[FromQuery, BindRequired] string pw ,
[FromQuery, BindRequired] string password )
{
var user = _userManager . GetUserById ( id ) ;
if ( user = = null )
{
return NotFound ( "User not found" ) ;
}
if ( ! string . IsNullOrEmpty ( password ) & & string . IsNullOrEmpty ( pw ) )
{
2020-06-18 16:09:58 +00:00
return Forbid ( "Only sha1 password is not allowed." ) ;
2020-06-17 19:08:58 +00:00
}
// Password should always be null
2020-06-18 16:09:58 +00:00
return await AuthenticateUserByName ( user . Username , pw , password ) . ConfigureAwait ( false ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Authenticates a user by name.
/// </summary>
2020-06-18 16:09:58 +00:00
/// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
/// <response code="200">User authenticated.</response>
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("AuthenticateByName")]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task < ActionResult < AuthenticationResult > > AuthenticateUserByName ( [ FromBody , BindRequired ] AuthenticateUserByName request )
2020-06-17 19:08:58 +00:00
{
var auth = _authContext . GetAuthorizationInfo ( Request ) ;
try
{
var result = await _sessionManager . AuthenticateNewSession ( new AuthenticationRequest
{
App = auth . Client ,
AppVersion = auth . Version ,
DeviceId = auth . DeviceId ,
DeviceName = auth . Device ,
2020-06-18 16:09:58 +00:00
Password = request . Pw ,
PasswordSha1 = request . Password ,
2020-06-17 19:08:58 +00:00
RemoteEndPoint = HttpContext . Connection . RemoteIpAddress . ToString ( ) ,
2020-06-18 16:09:58 +00:00
Username = request . Username
2020-06-17 19:08:58 +00:00
} ) . ConfigureAwait ( false ) ;
return Ok ( result ) ;
}
catch ( SecurityException e )
{
// rethrow adding IP address to message
throw new SecurityException ( $"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}" , e ) ;
}
}
/// <summary>
/// Updates a user's password.
/// </summary>
2020-06-18 16:09:58 +00:00
/// <param name="id">The user id.</param>
/// <param name="currentPassword">The current password sha1-hash.</param>
/// <param name="currentPw">The current password as plain text.</param>
/// <param name="newPw">The new password in plain text.</param>
2020-06-17 19:08:58 +00:00
/// <param name="resetPassword">Whether to reset the password.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("{id}/Password")]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-06-17 19:08:58 +00:00
public async Task < ActionResult > UpdateUserPassword (
[FromRoute] Guid id ,
2020-06-18 16:09:58 +00:00
[FromBody] string currentPassword ,
[FromBody] string currentPw ,
[FromBody] string newPw ,
[FromBody] bool resetPassword )
2020-06-17 19:08:58 +00:00
{
2020-06-18 16:09:58 +00:00
if ( ! RequestHelpers . AssertCanUpdateUser ( _authContext , HttpContext . Request , id , true ) )
{
return Forbid ( "User is not allowed to update the password." ) ;
}
2020-06-17 19:08:58 +00:00
var user = _userManager . GetUserById ( id ) ;
if ( user = = null )
{
return NotFound ( "User not found" ) ;
}
if ( resetPassword )
{
await _userManager . ResetPassword ( user ) . ConfigureAwait ( false ) ;
}
else
{
var success = await _userManager . AuthenticateUser (
user . Username ,
currentPw ,
currentPassword ,
HttpContext . Connection . RemoteIpAddress . ToString ( ) ,
false ) . ConfigureAwait ( false ) ;
if ( success = = null )
{
2020-06-18 16:09:58 +00:00
return Forbid ( "Invalid user or password entered." ) ;
2020-06-17 19:08:58 +00:00
}
await _userManager . ChangePassword ( user , newPw ) . ConfigureAwait ( false ) ;
var currentToken = _authContext . GetAuthorizationInfo ( Request ) . Token ;
_sessionManager . RevokeUserTokens ( user . Id , currentToken ) ;
}
return NoContent ( ) ;
}
/// <summary>
/// Updates a user's easy password.
/// </summary>
2020-06-18 16:09:58 +00:00
/// <param name="id">The user id.</param>
/// <param name="newPassword">The new password sha1-hash.</param>
/// <param name="newPw">The new password in plain text.</param>
/// <param name="resetPassword">Whether to reset the password.</param>
/// <response code="200">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("{id}/EasyPassword")]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-06-17 19:08:58 +00:00
public ActionResult UpdateUserEasyPassword (
[FromRoute] Guid id ,
2020-06-18 16:09:58 +00:00
[FromBody] string newPassword ,
[FromBody] string newPw ,
[FromBody] bool resetPassword )
2020-06-17 19:08:58 +00:00
{
2020-06-18 16:09:58 +00:00
if ( ! RequestHelpers . AssertCanUpdateUser ( _authContext , HttpContext . Request , id , true ) )
{
return Forbid ( "User is not allowed to update the easy password." ) ;
}
2020-06-17 19:08:58 +00:00
var user = _userManager . GetUserById ( id ) ;
if ( user = = null )
{
return NotFound ( "User not found" ) ;
}
if ( resetPassword )
{
_userManager . ResetEasyPassword ( user ) ;
}
else
{
_userManager . ChangeEasyPassword ( user , newPw , newPassword ) ;
}
return NoContent ( ) ;
}
/// <summary>
/// Updates a user.
/// </summary>
2020-06-18 16:09:58 +00:00
/// <param name="id">The user id.</param>
/// <param name="updateUser">The updated user model.</param>
/// <response code="204">User updated.</response>
/// <response code="400">User information was not supplied.</response>
/// <response code="403">User update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("{id}")]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task < ActionResult > UpdateUser (
[FromRoute] Guid id ,
[FromBody] UserDto updateUser )
2020-06-17 19:08:58 +00:00
{
2020-06-18 16:09:58 +00:00
if ( updateUser = = null )
{
return BadRequest ( ) ;
}
if ( ! RequestHelpers . AssertCanUpdateUser ( _authContext , HttpContext . Request , id , false ) )
{
return Forbid ( "User update not allowed." ) ;
}
var user = _userManager . GetUserById ( id ) ;
if ( string . Equals ( user . Username , updateUser . Name , StringComparison . Ordinal ) )
{
await _userManager . UpdateUserAsync ( user ) . ConfigureAwait ( false ) ;
_userManager . UpdateConfiguration ( user . Id , updateUser . Configuration ) ;
}
else
{
await _userManager . RenameUser ( user , updateUser . Name ) . ConfigureAwait ( false ) ;
_userManager . UpdateConfiguration ( updateUser . Id , updateUser . Configuration ) ;
}
return NoContent ( ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Updates a user policy.
/// </summary>
/// <param name="id">The user id.</param>
2020-06-18 16:09:58 +00:00
/// <param name="newPolicy">The new user policy.</param>
/// <response code="204">User policy updated.</response>
/// <response code="400">User policy was not supplied.</response>
/// <response code="403">User policy update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("{id}/Policy")]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserPolicy (
[FromRoute] Guid id ,
[FromBody] UserPolicy newPolicy )
2020-06-17 19:08:58 +00:00
{
2020-06-18 16:09:58 +00:00
if ( newPolicy = = null )
{
return BadRequest ( ) ;
}
var user = _userManager . GetUserById ( id ) ;
// If removing admin access
if ( ! ( newPolicy . IsAdministrator & & user . HasPermission ( PermissionKind . IsAdministrator ) ) )
{
if ( _userManager . Users . Count ( i = > i . HasPermission ( PermissionKind . IsAdministrator ) ) = = 1 )
{
return Forbid ( "There must be at least one user in the system with administrative access." ) ;
}
}
// If disabling
if ( newPolicy . IsDisabled & & user . HasPermission ( PermissionKind . IsAdministrator ) )
{
return Forbid ( "Administrators cannot be disabled." ) ;
}
// If disabling
if ( newPolicy . IsDisabled & & ! user . HasPermission ( PermissionKind . IsDisabled ) )
{
if ( _userManager . Users . Count ( i = > ! i . HasPermission ( PermissionKind . IsDisabled ) ) = = 1 )
{
return Forbid ( "There must be at least one enabled user in the system." ) ;
}
var currentToken = _authContext . GetAuthorizationInfo ( Request ) . Token ;
_sessionManager . RevokeUserTokens ( user . Id , currentToken ) ;
}
_userManager . UpdatePolicy ( id , newPolicy ) ;
return NoContent ( ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Updates a user configuration.
/// </summary>
/// <param name="id">The user id.</param>
2020-06-18 16:09:58 +00:00
/// <param name="userConfig">The new user configuration.</param>
/// <response code="204">User configuration updated.</response>
/// <response code="403">User configuration update forbidden.</response>
2020-06-17 19:08:58 +00:00
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{id}/Configuration")]
[Authorize]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserConfiguration (
[FromRoute] Guid id ,
[FromBody] UserConfiguration userConfig )
2020-06-17 19:08:58 +00:00
{
2020-06-18 16:09:58 +00:00
if ( ! RequestHelpers . AssertCanUpdateUser ( _authContext , HttpContext . Request , id , false ) )
{
return Forbid ( "User configuration update not allowed" ) ;
}
_userManager . UpdateConfiguration ( id , userConfig ) ;
return NoContent ( ) ;
2020-06-17 19:08:58 +00:00
}
/// <summary>
/// Creates a user.
/// </summary>
/// <param name="name">The username.</param>
/// <param name="password">The password.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">User created.</response>
/// <returns>An <see cref="UserDto"/> of the new user.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("/Users/New")]
[Authorize(Policy = Policies.RequiresElevation)]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task < ActionResult < UserDto > > CreateUserByName (
2020-06-17 19:08:58 +00:00
[FromBody] string name ,
[FromBody] string password )
{
var newUser = _userManager . CreateUser ( name ) ;
// no need to authenticate password for new user
if ( password ! = null )
{
await _userManager . ChangePassword ( newUser , password ) . ConfigureAwait ( false ) ;
}
var result = _userManager . GetUserDto ( newUser , HttpContext . Connection . RemoteIpAddress . ToString ( ) ) ;
return Ok ( result ) ;
}
/// <summary>
/// Initiates the forgot password process for a local user.
/// </summary>
/// <param name="enteredUsername">The entered username.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">Password reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("ForgotPassword")]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-06-17 19:08:58 +00:00
public async Task < ActionResult < ForgotPasswordResult > > ForgotPassword ( [ FromBody ] string enteredUsername )
{
var isLocal = HttpContext . Connection . RemoteIpAddress . Equals ( HttpContext . Connection . LocalIpAddress )
| | _networkManager . IsInLocalNetwork ( HttpContext . Connection . RemoteIpAddress . ToString ( ) ) ;
var result = await _userManager . StartForgotPasswordProcess ( enteredUsername , isLocal ) . ConfigureAwait ( false ) ;
return Ok ( result ) ;
}
/// <summary>
/// Redeems a forgot password pin.
/// </summary>
/// <param name="pin">The pin.</param>
2020-06-18 16:09:58 +00:00
/// <response code="200">Pin reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
2020-06-17 19:08:58 +00:00
[HttpPost("ForgotPassword/Pin")]
2020-06-18 16:09:58 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-06-17 19:08:58 +00:00
public async Task < ActionResult < PinRedeemResult > > ForgotPasswordPin ( [ FromBody ] string pin )
{
var result = await _userManager . RedeemPasswordResetPin ( pin ) . ConfigureAwait ( false ) ;
return Ok ( result ) ;
}
2020-06-18 16:09:58 +00:00
private IEnumerable < UserDto > Get ( bool? isHidden , bool? isDisabled , bool filterByDevice , bool filterByNetwork )
2020-06-17 19:08:58 +00:00
{
var users = _userManager . Users ;
if ( isDisabled . HasValue )
{
users = users . Where ( i = > i . HasPermission ( PermissionKind . IsDisabled ) = = isDisabled . Value ) ;
}
if ( isHidden . HasValue )
{
users = users . Where ( i = > i . HasPermission ( PermissionKind . IsHidden ) = = isHidden . Value ) ;
}
if ( filterByDevice )
{
var deviceId = _authContext . GetAuthorizationInfo ( Request ) . DeviceId ;
if ( ! string . IsNullOrWhiteSpace ( deviceId ) )
{
users = users . Where ( i = > _deviceManager . CanAccessDevice ( i , deviceId ) ) ;
}
}
if ( filterByNetwork )
{
if ( ! _networkManager . IsInLocalNetwork ( HttpContext . Connection . RemoteIpAddress . ToString ( ) ) )
{
users = users . Where ( i = > i . HasPermission ( PermissionKind . EnableRemoteAccess ) ) ;
}
}
var result = users
. OrderBy ( u = > u . Username )
. Select ( i = > _userManager . GetUserDto ( i , HttpContext . Connection . RemoteIpAddress . ToString ( ) ) )
. ToArray ( ) ;
return result ;
}
}
}