2020-06-12 16:54:25 +00:00
#pragma warning disable CA1801
using System ;
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
using System.Threading ;
2020-06-22 13:44:11 +00:00
using Jellyfin.Api.Constants ;
2020-06-12 16:54:25 +00:00
using Jellyfin.Api.Helpers ;
2020-06-19 11:03:53 +00:00
using Jellyfin.Data.Enums ;
2020-06-12 16:54:25 +00:00
using MediaBrowser.Controller.Devices ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Net ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Model.Dto ;
using MediaBrowser.Model.Session ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc ;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The session controller.
/// </summary>
2020-08-04 18:48:53 +00:00
[Route("")]
2020-06-12 16:54:25 +00:00
public class SessionController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager ;
private readonly IUserManager _userManager ;
private readonly IAuthorizationContext _authContext ;
private readonly IDeviceManager _deviceManager ;
/// <summary>
/// Initializes a new instance of the <see cref="SessionController"/> class.
/// </summary>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
/// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
public SessionController (
ISessionManager sessionManager ,
IUserManager userManager ,
IAuthorizationContext authContext ,
2020-06-19 11:03:53 +00:00
IDeviceManager deviceManager )
2020-06-12 16:54:25 +00:00
{
_sessionManager = sessionManager ;
_userManager = userManager ;
_authContext = authContext ;
_deviceManager = deviceManager ;
}
/// <summary>
/// Gets a list of sessions.
/// </summary>
/// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
/// <param name="deviceId">Filter by device Id.</param>
/// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
/// <response code="200">List of sessions returned.</response>
/// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
2020-08-04 18:48:53 +00:00
[HttpGet("Sessions")]
2020-06-22 13:44:11 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult < IEnumerable < SessionInfo > > GetSessions (
2020-07-07 15:10:51 +00:00
[FromQuery] Guid ? controllableByUserId ,
2020-06-27 16:50:44 +00:00
[FromQuery] string? deviceId ,
2020-06-12 16:54:25 +00:00
[FromQuery] int? activeWithinSeconds )
{
var result = _sessionManager . Sessions ;
if ( ! string . IsNullOrEmpty ( deviceId ) )
{
result = result . Where ( i = > string . Equals ( i . DeviceId , deviceId , StringComparison . OrdinalIgnoreCase ) ) ;
}
2020-07-07 15:10:51 +00:00
if ( controllableByUserId . HasValue & & ! controllableByUserId . Equals ( Guid . Empty ) )
2020-06-12 16:54:25 +00:00
{
result = result . Where ( i = > i . SupportsRemoteControl ) ;
2020-07-07 15:10:51 +00:00
var user = _userManager . GetUserById ( controllableByUserId . Value ) ;
2020-06-12 16:54:25 +00:00
2020-06-19 11:03:53 +00:00
if ( ! user . HasPermission ( PermissionKind . EnableRemoteControlOfOtherUsers ) )
2020-06-12 16:54:25 +00:00
{
2020-07-07 15:10:51 +00:00
result = result . Where ( i = > i . UserId . Equals ( Guid . Empty ) | | i . ContainsUser ( controllableByUserId . Value ) ) ;
2020-06-12 16:54:25 +00:00
}
2020-06-19 11:03:53 +00:00
if ( ! user . HasPermission ( PermissionKind . EnableSharedDeviceControl ) )
2020-06-12 16:54:25 +00:00
{
result = result . Where ( i = > ! i . UserId . Equals ( Guid . Empty ) ) ;
}
if ( activeWithinSeconds . HasValue & & activeWithinSeconds . Value > 0 )
{
var minActiveDate = DateTime . UtcNow . AddSeconds ( 0 - activeWithinSeconds . Value ) ;
result = result . Where ( i = > i . LastActivityDate > = minActiveDate ) ;
}
result = result . Where ( i = >
{
if ( ! string . IsNullOrWhiteSpace ( i . DeviceId ) )
{
if ( ! _deviceManager . CanAccessDevice ( user , i . DeviceId ) )
{
return false ;
}
}
return true ;
} ) ;
}
return Ok ( result ) ;
}
/// <summary>
/// Instructs a session to browse to an item or view.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session Id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="itemType">The type of item to browse to.</param>
/// <param name="itemId">The Id of the item.</param>
/// <param name="itemName">The name of the item.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Viewing")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DisplayContent (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
[FromQuery, Required] string? itemType ,
[FromQuery, Required] string? itemId ,
[FromQuery, Required] string? itemName )
2020-06-12 16:54:25 +00:00
{
var command = new BrowseRequest
{
ItemId = itemId ,
ItemName = itemName ,
ItemType = itemType
} ;
_sessionManager . SendBrowseCommand (
2020-06-19 11:03:53 +00:00
RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ,
2020-06-21 00:02:07 +00:00
sessionId ,
2020-06-12 16:54:25 +00:00
command ,
CancellationToken . None ) ;
return NoContent ( ) ;
}
/// <summary>
/// Instructs a session to play an item.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="playRequest">The <see cref="PlayRequest"/>.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Playing")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Play (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
2020-06-12 16:54:25 +00:00
[FromQuery] Guid [ ] itemIds ,
[FromQuery] long? startPositionTicks ,
[FromQuery] PlayCommand playCommand ,
[FromBody, Required] PlayRequest playRequest )
{
if ( playRequest = = null )
{
throw new ArgumentException ( "Request Body may not be null" ) ;
}
playRequest . ItemIds = itemIds ;
playRequest . StartPositionTicks = startPositionTicks ;
playRequest . PlayCommand = playCommand ;
_sessionManager . SendPlayCommand (
2020-06-19 11:03:53 +00:00
RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ,
2020-06-21 00:02:07 +00:00
sessionId ,
2020-06-12 16:54:25 +00:00
playRequest ,
CancellationToken . None ) ;
return NoContent ( ) ;
}
/// <summary>
/// Issues a playstate command to a client.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
/// <response code="204">Playstate command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendPlaystateCommand (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
2020-06-12 16:54:25 +00:00
[FromBody] PlaystateRequest playstateRequest )
{
_sessionManager . SendPlaystateCommand (
2020-06-19 11:03:53 +00:00
RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ,
2020-06-21 00:02:07 +00:00
sessionId ,
2020-06-12 16:54:25 +00:00
playstateRequest ,
CancellationToken . None ) ;
return NoContent ( ) ;
}
/// <summary>
/// Issues a system command to a client.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="command">The command to send.</param>
/// <response code="204">System command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/System/{command}")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
[FromRoute, Required] string? command )
2020-06-12 16:54:25 +00:00
{
var name = command ;
if ( Enum . TryParse ( name , true , out GeneralCommandType commandType ) )
{
name = commandType . ToString ( ) ;
}
2020-06-19 11:03:53 +00:00
var currentSession = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) ;
2020-06-12 16:54:25 +00:00
var generalCommand = new GeneralCommand
{
Name = name ,
ControllingUserId = currentSession . UserId
} ;
2020-06-21 00:02:07 +00:00
_sessionManager . SendGeneralCommand ( currentSession . Id , sessionId , generalCommand , CancellationToken . None ) ;
2020-06-12 16:54:25 +00:00
return NoContent ( ) ;
}
/// <summary>
/// Issues a general command to a client.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="command">The command to send.</param>
/// <response code="204">General command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Command/{command}")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
[FromRoute, Required] string? command )
2020-06-12 16:54:25 +00:00
{
2020-06-19 11:03:53 +00:00
var currentSession = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) ;
2020-06-12 16:54:25 +00:00
var generalCommand = new GeneralCommand
{
Name = command ,
ControllingUserId = currentSession . UserId
} ;
2020-06-21 00:02:07 +00:00
_sessionManager . SendGeneralCommand ( currentSession . Id , sessionId , generalCommand , CancellationToken . None ) ;
2020-06-12 16:54:25 +00:00
return NoContent ( ) ;
}
/// <summary>
/// Issues a full general command to a client.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="command">The <see cref="GeneralCommand"/>.</param>
/// <response code="204">Full general command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Command")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendFullGeneralCommand (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
2020-06-12 16:54:25 +00:00
[FromBody, Required] GeneralCommand command )
{
2020-06-19 11:03:53 +00:00
var currentSession = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) ;
2020-06-12 16:54:25 +00:00
if ( command = = null )
{
throw new ArgumentException ( "Request body may not be null" ) ;
}
command . ControllingUserId = currentSession . UserId ;
_sessionManager . SendGeneralCommand (
currentSession . Id ,
2020-06-21 00:02:07 +00:00
sessionId ,
2020-06-12 16:54:25 +00:00
command ,
CancellationToken . None ) ;
return NoContent ( ) ;
}
/// <summary>
/// Issues a command to a client to display a message to the user.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="text">The message test.</param>
/// <param name="header">The message header.</param>
/// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
/// <response code="204">Message sent.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/Message")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
[FromQuery, Required] string? text ,
[FromQuery, Required] string? header ,
2020-06-12 16:54:25 +00:00
[FromQuery] long? timeoutMs )
{
var command = new MessageCommand
{
Header = string . IsNullOrEmpty ( header ) ? "Message from Server" : header ,
TimeoutMs = timeoutMs ,
Text = text
} ;
2020-06-21 00:02:07 +00:00
_sessionManager . SendMessageCommand ( RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id , sessionId , command , CancellationToken . None ) ;
2020-06-12 16:54:25 +00:00
return NoContent ( ) ;
}
/// <summary>
/// Adds an additional user to a session.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="userId">The user id.</param>
/// <response code="204">User added to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/{sessionId}/User/{userId}")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession (
2020-08-06 14:17:45 +00:00
[FromRoute, Required] string? sessionId ,
2020-06-12 16:54:25 +00:00
[FromRoute] Guid userId )
{
2020-06-21 00:02:07 +00:00
_sessionManager . AddAdditionalUser ( sessionId , userId ) ;
2020-06-12 16:54:25 +00:00
return NoContent ( ) ;
}
/// <summary>
/// Removes an additional user from a session.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="sessionId">The session id.</param>
2020-06-12 16:54:25 +00:00
/// <param name="userId">The user id.</param>
/// <response code="204">User removed from session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpDelete("Sessions/{sessionId}/User/{userId}")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession (
2020-06-27 16:50:44 +00:00
[FromRoute] string? sessionId ,
2020-06-12 16:54:25 +00:00
[FromRoute] Guid userId )
{
2020-06-21 00:02:07 +00:00
_sessionManager . RemoveAdditionalUser ( sessionId , userId ) ;
2020-06-12 16:54:25 +00:00
return NoContent ( ) ;
}
/// <summary>
/// Updates capabilities for a device.
/// </summary>
/// <param name="id">The session id.</param>
/// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param>
/// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param>
/// <param name="supportsMediaControl">Determines whether media can be played remotely..</param>
/// <param name="supportsSync">Determines whether sync is supported.</param>
/// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
/// <response code="204">Capabilities posted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/Capabilities")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities (
2020-08-06 14:17:45 +00:00
[FromQuery, Required] string? id ,
2020-06-27 16:50:44 +00:00
[FromQuery] string? playableMediaTypes ,
[FromQuery] string? supportedCommands ,
2020-07-07 15:10:51 +00:00
[FromQuery] bool supportsMediaControl = false ,
[FromQuery] bool supportsSync = false ,
2020-06-12 16:54:25 +00:00
[FromQuery] bool supportsPersistentIdentifier = true )
{
if ( string . IsNullOrWhiteSpace ( id ) )
{
2020-06-19 11:03:53 +00:00
id = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ;
2020-06-12 16:54:25 +00:00
}
_sessionManager . ReportCapabilities ( id , new ClientCapabilities
{
PlayableMediaTypes = RequestHelpers . Split ( playableMediaTypes , ',' , true ) ,
SupportedCommands = RequestHelpers . Split ( supportedCommands , ',' , true ) ,
SupportsMediaControl = supportsMediaControl ,
SupportsSync = supportsSync ,
SupportsPersistentIdentifier = supportsPersistentIdentifier
} ) ;
return NoContent ( ) ;
}
/// <summary>
/// Updates capabilities for a device.
/// </summary>
/// <param name="id">The session id.</param>
/// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
/// <response code="204">Capabilities updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/Capabilities/Full")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities (
2020-08-09 20:12:39 +00:00
[FromQuery] string? id ,
2020-06-12 16:54:25 +00:00
[FromBody, Required] ClientCapabilities capabilities )
{
if ( string . IsNullOrWhiteSpace ( id ) )
{
2020-06-19 11:03:53 +00:00
id = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ;
2020-06-12 16:54:25 +00:00
}
_sessionManager . ReportCapabilities ( id , capabilities ) ;
return NoContent ( ) ;
}
/// <summary>
/// Reports that a session is viewing an item.
/// </summary>
/// <param name="sessionId">The session id.</param>
/// <param name="itemId">The item id.</param>
/// <response code="204">Session reported to server.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/Viewing")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing (
2020-06-27 16:50:44 +00:00
[FromQuery] string? sessionId ,
[FromQuery] string? itemId )
2020-06-12 16:54:25 +00:00
{
2020-06-19 11:03:53 +00:00
string session = RequestHelpers . GetSession ( _sessionManager , _authContext , Request ) . Id ;
2020-06-12 16:54:25 +00:00
_sessionManager . ReportNowViewingItem ( session , itemId ) ;
return NoContent ( ) ;
}
/// <summary>
/// Reports that a session has ended.
/// </summary>
/// <response code="204">Session end reported to server.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
2020-08-04 18:48:53 +00:00
[HttpPost("Sessions/Logout")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportSessionEnded ( )
{
AuthorizationInfo auth = _authContext . GetAuthorizationInfo ( Request ) ;
_sessionManager . Logout ( auth . Token ) ;
return NoContent ( ) ;
}
/// <summary>
/// Get all auth providers.
/// </summary>
/// <response code="200">Auth providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
2020-08-04 18:48:53 +00:00
[HttpGet("Auth/Providers")]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.RequiresElevation)]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult < IEnumerable < NameIdPair > > GetAuthProviders ( )
{
return _userManager . GetAuthenticationProviders ( ) ;
}
/// <summary>
/// Get all password reset providers.
/// </summary>
/// <response code="200">Password reset providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
2020-08-10 16:09:27 +00:00
[HttpGet("Auth/PasswordResetProviders")]
2020-06-12 16:54:25 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-08-06 14:17:45 +00:00
[Authorize(Policy = Policies.RequiresElevation)]
2020-06-12 16:54:25 +00:00
public ActionResult < IEnumerable < NameIdPair > > GetPasswordResetProviders ( )
{
return _userManager . GetPasswordResetProviders ( ) ;
}
}
}