From 86d68e23e7af367152edc36977a9a39431bd2641 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:06:18 -0600 Subject: [PATCH 01/25] Add DisplayPreferencesController --- .../Controllers/DisplayPreferencesController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Jellyfin.Api/Controllers/DisplayPreferencesController.cs diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs new file mode 100644 index 0000000000..537a940460 --- /dev/null +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Display Preferences Controller. + /// + public class DisplayPreferencesController : BaseJellyfinApiController + { + } +} From a282fbe9668263481b850b29b3fb8064d4d7ee9f Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:26:38 -0600 Subject: [PATCH 02/25] Move DisplayPreferences to Jellyfin.Api --- .../DisplayPreferencesController.cs | 92 ++++++++++++++++ MediaBrowser.Api/DisplayPreferencesService.cs | 101 ------------------ 2 files changed, 92 insertions(+), 101 deletions(-) delete mode 100644 MediaBrowser.Api/DisplayPreferencesService.cs diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 537a940460..6182c3507b 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,4 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -7,5 +14,90 @@ namespace Jellyfin.Api.Controllers /// public class DisplayPreferencesController : BaseJellyfinApiController { + private readonly IDisplayPreferencesRepository _displayPreferencesRepository; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository) + { + _displayPreferencesRepository = displayPreferencesRepository; + } + + /// + /// Get Display Preferences + /// + /// Display preferences id. + /// User id. + /// Client. + /// Display Preferences. + [HttpGet("{DisplayPreferencesId")] + [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetDisplayPreferences( + [FromRoute] string displayPreferencesId, + [FromQuery] [Required] string userId, + [FromQuery] [Required] string client + ) + { + try + { + var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); + if (result == null) + { + return NotFound(); + } + + // TODO ToOptimizedResult + return Ok(result); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Update Display Preferences + /// + /// Display preferences id. + /// User Id. + /// Client. + /// New Display Preferences object. + /// Status. + [HttpPost("{DisplayPreferencesId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult UpdateDisplayPreferences( + [FromRoute] string displayPreferencesId, + [FromQuery, BindRequired] string userId, + [FromQuery, BindRequired] string client, + [FromBody, BindRequired] DisplayPreferences displayPreferences) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + displayPreferences.Id = displayPreferencesId; + _displayPreferencesRepository.SaveDisplayPreferences( + displayPreferences, + userId, + client, + CancellationToken.None); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } } } diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs deleted file mode 100644 index 62c4ff43f2..0000000000 --- a/MediaBrowser.Api/DisplayPreferencesService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Threading; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class UpdateDisplayPreferences - /// - [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")] - public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string DisplayPreferencesId { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")] - public class GetDisplayPreferences : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string UserId { get; set; } - - [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Client { get; set; } - } - - /// - /// Class DisplayPreferencesService - /// - [Authenticated] - public class DisplayPreferencesService : BaseApiService - { - /// - /// The _display preferences manager - /// - private readonly IDisplayPreferencesRepository _displayPreferencesManager; - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Initializes a new instance of the class. - /// - /// The json serializer. - /// The display preferences manager. - public DisplayPreferencesService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IDisplayPreferencesRepository displayPreferencesManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _jsonSerializer = jsonSerializer; - _displayPreferencesManager = displayPreferencesManager; - } - - /// - /// Gets the specified request. - /// - /// The request. - public object Get(GetDisplayPreferences request) - { - var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateDisplayPreferences request) - { - // Serialize to json and then back so that the core doesn't see the request dto type - var displayPreferences = _jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(request)); - - _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None); - } - } -} From c31b9f5169ae62787fa356ccecc2f1fc6896d04b Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:30:10 -0600 Subject: [PATCH 03/25] Fix build & runtime errors --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 6182c3507b..a3bcafaea5 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -26,21 +26,20 @@ namespace Jellyfin.Api.Controllers } /// - /// Get Display Preferences + /// Get Display Preferences. /// /// Display preferences id. /// User id. /// Client. /// Display Preferences. - [HttpGet("{DisplayPreferencesId")] + [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult GetDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery] [Required] string userId, - [FromQuery] [Required] string client - ) + [FromQuery] [Required] string client) { try { @@ -60,7 +59,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Update Display Preferences + /// Update Display Preferences. /// /// Display preferences id. /// User Id. From 60607ab60c3051815179859adfd2a7182f9ceb9a Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:34:34 -0600 Subject: [PATCH 04/25] Fix saving DisplayPreferences --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index a3bcafaea5..2c4072b39f 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -84,7 +84,11 @@ namespace Jellyfin.Api.Controllers return BadRequest(ModelState); } - displayPreferences.Id = displayPreferencesId; + if (displayPreferencesId == null) + { + // do nothing. + } + _displayPreferencesRepository.SaveDisplayPreferences( displayPreferences, userId, From e6b873f2aeadd01ed4638148be857ddf45a33576 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 12:56:16 -0600 Subject: [PATCH 05/25] Fix missing attributes --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2c4072b39f..0fbdcb6b80 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,6 +1,9 @@ +#nullable enable + using System; using System.ComponentModel.DataAnnotations; using System.Threading; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; @@ -12,6 +15,7 @@ namespace Jellyfin.Api.Controllers /// /// Display Preferences Controller. /// + [Authenticated] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesRepository _displayPreferencesRepository; From c5d709f77ed2158bf68b8cc81238067d4525518f Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 16:35:31 -0600 Subject: [PATCH 06/25] remove todo --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0fbdcb6b80..0554091b45 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -53,7 +53,6 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - // TODO ToOptimizedResult return Ok(result); } catch (Exception e) From 04119c0d409342050cb7624f025a21985e10a412 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:55:57 -0600 Subject: [PATCH 07/25] Remove exception handler --- .../DisplayPreferencesController.cs | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0554091b45..e15e9c4be6 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using System.ComponentModel.DataAnnotations; using System.Threading; using MediaBrowser.Controller.Net; @@ -45,20 +44,13 @@ namespace Jellyfin.Api.Controllers [FromQuery] [Required] string userId, [FromQuery] [Required] string client) { - try + var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); + if (result == null) { - var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); - if (result == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(result); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(result); } /// @@ -80,30 +72,23 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { - try + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (displayPreferencesId == null) - { - // do nothing. - } - - _displayPreferencesRepository.SaveDisplayPreferences( - displayPreferences, - userId, - client, - CancellationToken.None); - - return Ok(); + return BadRequest(ModelState); } - catch (Exception e) + + if (displayPreferencesId == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + // do nothing. } + + _displayPreferencesRepository.SaveDisplayPreferences( + displayPreferences, + userId, + client, + CancellationToken.None); + + return Ok(); } } } From 98224dee9e3bfc2bb30c14792aec4bda47670863 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:01:47 -0600 Subject: [PATCH 08/25] move to ActionResult --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index e15e9c4be6..25391bcf84 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -36,10 +36,9 @@ namespace Jellyfin.Api.Controllers /// Client. /// Display Preferences. [HttpGet("{DisplayPreferencesId}")] - [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetDisplayPreferences( + public ActionResult GetDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery] [Required] string userId, [FromQuery] [Required] string client) @@ -65,8 +64,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateDisplayPreferences( + public ActionResult UpdateDisplayPreferences( [FromRoute] string displayPreferencesId, [FromQuery, BindRequired] string userId, [FromQuery, BindRequired] string client, From 1223eb5a2285c48f50b07fb5aa2c463928b69afe Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 08:03:41 -0600 Subject: [PATCH 09/25] Remove unneeded Ok calls. --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 25391bcf84..42e87edd6f 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(result); + return result; } /// From 311f2e2bc317cea7ac4d4cc783b961793bb997d5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:07:21 -0600 Subject: [PATCH 10/25] Fix Authorize attributes --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 42e87edd6f..0d375e668a 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -2,9 +2,9 @@ using System.ComponentModel.DataAnnotations; using System.Threading; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Controllers /// /// Display Preferences Controller. /// - [Authenticated] + [Authorize] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesRepository _displayPreferencesRepository; From 0017163f39438e2718f7c95b3fb65df5dde65e3d Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:06:29 -0600 Subject: [PATCH 11/25] Update endpoint docs --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0d375e668a..2837ea8e87 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -34,7 +34,9 @@ namespace Jellyfin.Api.Controllers /// Display preferences id. /// User id. /// Client. - /// Display Preferences. + /// Display preferences retrieved. + /// Specified display preferences not found. + /// An containing the display preferences on success, or a if the display preferences could not be found. [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -59,7 +61,9 @@ namespace Jellyfin.Api.Controllers /// User Id. /// Client. /// New Display Preferences object. - /// Status. + /// Display preferences updated. + /// Specified display preferences not found. + /// An on success, or a if the display preferences could not be found. [HttpPost("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)] From 1c471d58551043dab3c808952d9834163cac3078 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:01:00 -0600 Subject: [PATCH 12/25] Clean UpdateDisplayPreferences endpoint --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2837ea8e87..579b5df5d4 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -74,14 +74,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - if (displayPreferencesId == null) { - // do nothing. + // TODO - refactor so parameter doesn't exist or is actually used. } _displayPreferencesRepository.SaveDisplayPreferences( From aed6f57f11e4d08372fcf456742bdaedea374f6d Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 20:54:02 -0600 Subject: [PATCH 13/25] Remove invalid docs and null check --- .../Controllers/DisplayPreferencesController.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 579b5df5d4..35efe6b5f8 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -35,7 +35,6 @@ namespace Jellyfin.Api.Controllers /// User id. /// Client. /// Display preferences retrieved. - /// Specified display preferences not found. /// An containing the display preferences on success, or a if the display preferences could not be found. [HttpGet("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -45,13 +44,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] [Required] string userId, [FromQuery] [Required] string client) { - var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); - if (result == null) - { - return NotFound(); - } - - return result; + return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); } /// @@ -62,7 +55,6 @@ namespace Jellyfin.Api.Controllers /// Client. /// New Display Preferences object. /// Display preferences updated. - /// Specified display preferences not found. /// An on success, or a if the display preferences could not be found. [HttpPost("{DisplayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] From fff3c789b98aad08f8ea66da275ff82c71dd8f2b Mon Sep 17 00:00:00 2001 From: David Date: Fri, 12 Jun 2020 18:54:25 +0200 Subject: [PATCH 14/25] Move SessionService to Jellyfin.Api --- Jellyfin.Api/Controllers/SessionController.cs | 478 +++++++++++++++++ Jellyfin.Api/Helpers/RequestHelpers.cs | 15 + MediaBrowser.Api/Sessions/SessionService.cs | 498 ------------------ 3 files changed, 493 insertions(+), 498 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SessionController.cs delete mode 100644 MediaBrowser.Api/Sessions/SessionService.cs diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs new file mode 100644 index 0000000000..5b60275eb6 --- /dev/null +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -0,0 +1,478 @@ +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Helpers; +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 +{ + /// + /// The session controller. + /// + public class SessionController : BaseJellyfinApiController + { + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly IAuthorizationContext _authContext; + private readonly IDeviceManager _deviceManager; + private readonly ISessionContext _sessionContext; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SessionController( + ISessionManager sessionManager, + IUserManager userManager, + IAuthorizationContext authContext, + IDeviceManager deviceManager, + ISessionContext sessionContext) + { + _sessionManager = sessionManager; + _userManager = userManager; + _authContext = authContext; + _deviceManager = deviceManager; + _sessionContext = sessionContext; + } + + /// + /// Gets a list of sessions. + /// + /// Filter by sessions that a given user is allowed to remote control. + /// Filter by device Id. + /// Optional. Filter by sessions that were active in the last n seconds. + /// List of sessions returned. + /// An with the available sessions. + [HttpGet("/Sessions")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSessions( + [FromQuery] Guid controllableByUserId, + [FromQuery] string deviceId, + [FromQuery] int? activeWithinSeconds) + { + var result = _sessionManager.Sessions; + + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (!controllableByUserId.Equals(Guid.Empty)) + { + result = result.Where(i => i.SupportsRemoteControl); + + var user = _userManager.GetUserById(controllableByUserId); + + if (!user.Policy.EnableRemoteControlOfOtherUsers) + { + result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId)); + } + + if (!user.Policy.EnableSharedDeviceControl) + { + 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); + } + + /// + /// Instructs a session to browse to an item or view. + /// + /// The session Id. + /// The type of item to browse to. + /// The Id of the item. + /// The name of the item. + /// Instruction sent to session. + /// A . + [HttpPost("/Sessions/{id}/Viewing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DisplayContent( + [FromRoute] string id, + [FromQuery] string itemType, + [FromQuery] string itemId, + [FromQuery] string itemName) + { + var command = new BrowseRequest + { + ItemId = itemId, + ItemName = itemName, + ItemType = itemType + }; + + _sessionManager.SendBrowseCommand( + RequestHelpers.GetSession(_sessionContext).Id, + id, + command, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Instructs a session to play an item. + /// + /// The session id. + /// The ids of the items to play, comma delimited. + /// The starting position of the first item. + /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. + /// The . + /// Instruction sent to session. + /// A . + [HttpPost("/Sessions/{id}/Playing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Play( + [FromRoute] string id, + [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( + RequestHelpers.GetSession(_sessionContext).Id, + id, + playRequest, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a playstate command to a client. + /// + /// The session id. + /// The . + /// Playstate command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Playing/{command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendPlaystateCommand( + [FromRoute] string id, + [FromBody] PlaystateRequest playstateRequest) + { + _sessionManager.SendPlaystateCommand( + RequestHelpers.GetSession(_sessionContext).Id, + id, + playstateRequest, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a system command to a client. + /// + /// The session id. + /// The command to send. + /// System command sent to session. + /// A . + [HttpPost("/Sessions/{id}/System/{Command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendSystemCommand( + [FromRoute] string id, + [FromRoute] string command) + { + var name = command; + if (Enum.TryParse(name, true, out GeneralCommandType commandType)) + { + name = commandType.ToString(); + } + + var currentSession = RequestHelpers.GetSession(_sessionContext); + var generalCommand = new GeneralCommand + { + Name = name, + ControllingUserId = currentSession.UserId + }; + + _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a general command to a client. + /// + /// The session id. + /// The command to send. + /// General command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Command/{Command}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendGeneralCommand( + [FromRoute] string id, + [FromRoute] string command) + { + var currentSession = RequestHelpers.GetSession(_sessionContext); + + var generalCommand = new GeneralCommand + { + Name = command, + ControllingUserId = currentSession.UserId + }; + + _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a full general command to a client. + /// + /// The session id. + /// The . + /// Full general command sent to session. + /// A . + [HttpPost("/Sessions/{id}/Command")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendFullGeneralCommand( + [FromRoute] string id, + [FromBody, Required] GeneralCommand command) + { + var currentSession = RequestHelpers.GetSession(_sessionContext); + + if (command == null) + { + throw new ArgumentException("Request body may not be null"); + } + + command.ControllingUserId = currentSession.UserId; + + _sessionManager.SendGeneralCommand( + currentSession.Id, + id, + command, + CancellationToken.None); + + return NoContent(); + } + + /// + /// Issues a command to a client to display a message to the user. + /// + /// The session id. + /// The message test. + /// The message header. + /// The message timeout. If omitted the user will have to confirm viewing the message. + /// Message sent. + /// A . + [HttpPost("/Sessions/{id}/Message")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SendMessageCommand( + [FromRoute] string id, + [FromQuery] string text, + [FromQuery] string header, + [FromQuery] long? timeoutMs) + { + var command = new MessageCommand + { + Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, + TimeoutMs = timeoutMs, + Text = text + }; + + _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None); + + return NoContent(); + } + + /// + /// Adds an additional user to a session. + /// + /// The session id. + /// The user id. + /// User added to session. + /// A . + [HttpPost("/Sessions/{id}/User/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddUserToSession( + [FromRoute] string id, + [FromRoute] Guid userId) + { + _sessionManager.AddAdditionalUser(id, userId); + return NoContent(); + } + + /// + /// Removes an additional user from a session. + /// + /// The session id. + /// The user id. + /// User removed from session. + /// A . + [HttpDelete("/Sessions/{id}/User/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveUserFromSession( + [FromRoute] string id, + [FromRoute] Guid userId) + { + _sessionManager.RemoveAdditionalUser(id, userId); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. + /// A list of supported remote control commands, comma delimited. + /// Determines whether media can be played remotely.. + /// Determines whether sync is supported. + /// Determines whether the device supports a unique identifier. + /// Capabilities posted. + /// A . + [HttpPost("/Sessions/Capabilities")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostCapabilities( + [FromQuery] string id, + [FromQuery] string playableMediaTypes, + [FromQuery] string supportedCommands, + [FromQuery] bool supportsMediaControl, + [FromQuery] bool supportsSync, + [FromQuery] bool supportsPersistentIdentifier = true) + { + if (string.IsNullOrWhiteSpace(id)) + { + id = RequestHelpers.GetSession(_sessionContext).Id; + } + + _sessionManager.ReportCapabilities(id, new ClientCapabilities + { + PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), + SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true), + SupportsMediaControl = supportsMediaControl, + SupportsSync = supportsSync, + SupportsPersistentIdentifier = supportsPersistentIdentifier + }); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// The . + /// Capabilities updated. + /// A . + [HttpPost("/Sessions/Capabilities/Full")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostFullCapabilities( + [FromQuery] string id, + [FromBody, Required] ClientCapabilities capabilities) + { + if (string.IsNullOrWhiteSpace(id)) + { + id = RequestHelpers.GetSession(_sessionContext).Id; + } + + _sessionManager.ReportCapabilities(id, capabilities); + + return NoContent(); + } + + /// + /// Reports that a session is viewing an item. + /// + /// The session id. + /// The item id. + /// Session reported to server. + /// A . + [HttpPost("/Sessions/Viewing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ReportViewing( + [FromQuery] string sessionId, + [FromQuery] string itemId) + { + string session = RequestHelpers.GetSession(_sessionContext).Id; + + _sessionManager.ReportNowViewingItem(session, itemId); + return NoContent(); + } + + /// + /// Reports that a session has ended. + /// + /// Session end reported to server. + /// A . + [HttpPost("/Sessions/Logout")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ReportSessionEnded() + { + // TODO: how do we get AuthorizationInfo without an IRequest? + AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); + + _sessionManager.Logout(auth.Token); + return NoContent(); + } + + /// + /// Get all auth providers. + /// + /// Auth providers retrieved. + /// An with the auth providers. + [HttpGet("/Auth/Providers")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAuthProviders() + { + return _userManager.GetAuthenticationProviders(); + } + + /// + /// Get all password reset providers. + /// + /// Password reset providers retrieved. + /// An with the password reset providers. + [HttpGet("/Auto/PasswordResetProviders")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPasswordResetProviders() + { + return _userManager.GetPasswordResetProviders(); + } + } +} diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 9f4d34f9c6..ae8ab37e8e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,6 @@ using System; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; namespace Jellyfin.Api.Helpers { @@ -25,5 +27,18 @@ namespace Jellyfin.Api.Helpers ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } + + internal static SessionInfo GetSession(ISessionContext sessionContext) + { + // TODO: how do we get a SessionInfo without IRequest? + SessionInfo session = sessionContext.GetSession("Request"); + + if (session == null) + { + throw new ArgumentException("Session not found."); + } + + return session; + } } } diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs deleted file mode 100644 index 020bb5042b..0000000000 --- a/MediaBrowser.Api/Sessions/SessionService.cs +++ /dev/null @@ -1,498 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Sessions -{ - /// - /// Class GetSessions. - /// - [Route("/Sessions", "GET", Summary = "Gets a list of sessions")] - [Authenticated] - public class GetSessions : IReturn - { - [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid ControllableByUserId { get; set; } - - [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string DeviceId { get; set; } - - public int? ActiveWithinSeconds { get; set; } - } - - /// - /// Class DisplayContent. - /// - [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")] - [Authenticated] - public class DisplayContent : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Artist, Genre, Studio, Person, or any kind of BaseItem - /// - /// The type of the item. - [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemType { get; set; } - - /// - /// Artist name, genre name, item Id, etc - /// - /// The item identifier. - [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { get; set; } - - /// - /// Gets or sets the name of the item. - /// - /// The name of the item. - [ApiMember(Name = "ItemName", Description = "The name of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemName { get; set; } - } - - [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")] - [Authenticated] - public class Play : PlayRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")] - [Authenticated] - public class SendPlaystateCommand : PlaystateRequest, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendSystemCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the command. - /// - /// The play command. - [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Command { get; set; } - } - - [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendGeneralCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the command. - /// - /// The play command. - [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Command { get; set; } - } - - [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")] - [Authenticated] - public class SendFullGeneralCommand : GeneralCommand, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")] - [Authenticated] - public class SendMessageCommand : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "Text", Description = "The message text.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Text { get; set; } - - [ApiMember(Name = "Header", Description = "The message header.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Header { get; set; } - - [ApiMember(Name = "TimeoutMs", Description = "The message timeout. If omitted the user will have to confirm viewing the message.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public long? TimeoutMs { get; set; } - } - - [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")] - [Authenticated] - public class AddUserToSession : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")] - [Authenticated] - public class RemoveUserFromSession : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - } - - [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")] - [Authenticated] - public class PostCapabilities : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayableMediaTypes { get; set; } - - [ApiMember(Name = "SupportedCommands", Description = "A list of supported remote control commands, comma delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string SupportedCommands { get; set; } - - [ApiMember(Name = "SupportsMediaControl", Description = "Determines whether media can be played remotely.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsMediaControl { get; set; } - - [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsSync { get; set; } - - [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool SupportsPersistentIdentifier { get; set; } - - public PostCapabilities() - { - SupportsPersistentIdentifier = true; - } - } - - [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")] - [Authenticated] - public class PostFullCapabilities : ClientCapabilities, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - } - - [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")] - [Authenticated] - public class ReportViewing : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { get; set; } - } - - [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")] - [Authenticated] - public class ReportSessionEnded : IReturnVoid - { - } - - [Route("/Auth/Providers", "GET")] - [Authenticated(Roles = "Admin")] - public class GetAuthProviders : IReturn - { - } - - [Route("/Auth/PasswordResetProviders", "GET")] - [Authenticated(Roles = "Admin")] - public class GetPasswordResetProviders : IReturn - { - } - - /// - /// Class SessionsService. - /// - public class SessionService : BaseApiService - { - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - private readonly IUserManager _userManager; - private readonly IAuthorizationContext _authContext; - private readonly IDeviceManager _deviceManager; - private readonly ISessionContext _sessionContext; - - public SessionService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionManager sessionManager, - IUserManager userManager, - IAuthorizationContext authContext, - IDeviceManager deviceManager, - ISessionContext sessionContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionManager = sessionManager; - _userManager = userManager; - _authContext = authContext; - _deviceManager = deviceManager; - _sessionContext = sessionContext; - } - - public object Get(GetAuthProviders request) - { - return _userManager.GetAuthenticationProviders(); - } - - public object Get(GetPasswordResetProviders request) - { - return _userManager.GetPasswordResetProviders(); - } - - public void Post(ReportSessionEnded request) - { - var auth = _authContext.GetAuthorizationInfo(Request); - - _sessionManager.Logout(auth.Token); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetSessions request) - { - var result = _sessionManager.Sessions; - - if (!string.IsNullOrEmpty(request.DeviceId)) - { - result = result.Where(i => string.Equals(i.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase)); - } - - if (!request.ControllableByUserId.Equals(Guid.Empty)) - { - result = result.Where(i => i.SupportsRemoteControl); - - var user = _userManager.GetUserById(request.ControllableByUserId); - - if (!user.Policy.EnableRemoteControlOfOtherUsers) - { - result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId)); - } - - if (!user.Policy.EnableSharedDeviceControl) - { - result = result.Where(i => !i.UserId.Equals(Guid.Empty)); - } - - if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - result = result.Where(i => - { - var deviceId = i.DeviceId; - - if (!string.IsNullOrWhiteSpace(deviceId)) - { - if (!_deviceManager.CanAccessDevice(user, deviceId)) - { - return false; - } - } - - return true; - }); - } - - return ToOptimizedResult(result.ToArray()); - } - - public Task Post(SendPlaystateCommand request) - { - return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(DisplayContent request) - { - var command = new BrowseRequest - { - ItemId = request.ItemId, - ItemName = request.ItemName, - ItemType = request.ItemType - }; - - return _sessionManager.SendBrowseCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(SendSystemCommand request) - { - var name = request.Command; - if (Enum.TryParse(name, true, out GeneralCommandType commandType)) - { - name = commandType.ToString(); - } - - var currentSession = GetSession(_sessionContext); - var command = new GeneralCommand - { - Name = name, - ControllingUserId = currentSession.UserId - }; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(SendMessageCommand request) - { - var command = new MessageCommand - { - Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header, - TimeoutMs = request.TimeoutMs, - Text = request.Text - }; - - return _sessionManager.SendMessageCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(Play request) - { - return _sessionManager.SendPlayCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None); - } - - public Task Post(SendGeneralCommand request) - { - var currentSession = GetSession(_sessionContext); - - var command = new GeneralCommand - { - Name = request.Command, - ControllingUserId = currentSession.UserId - }; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None); - } - - public Task Post(SendFullGeneralCommand request) - { - var currentSession = GetSession(_sessionContext); - - request.ControllingUserId = currentSession.UserId; - - return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, request, CancellationToken.None); - } - - public void Post(AddUserToSession request) - { - _sessionManager.AddAdditionalUser(request.Id, new Guid(request.UserId)); - } - - public void Delete(RemoveUserFromSession request) - { - _sessionManager.RemoveAdditionalUser(request.Id, new Guid(request.UserId)); - } - - public void Post(PostCapabilities request) - { - if (string.IsNullOrWhiteSpace(request.Id)) - { - request.Id = GetSession(_sessionContext).Id; - } - - _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities - { - PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','), - SupportedCommands = SplitValue(request.SupportedCommands, ','), - SupportsMediaControl = request.SupportsMediaControl, - SupportsSync = request.SupportsSync, - SupportsPersistentIdentifier = request.SupportsPersistentIdentifier - }); - } - - public void Post(PostFullCapabilities request) - { - if (string.IsNullOrWhiteSpace(request.Id)) - { - request.Id = GetSession(_sessionContext).Id; - } - - _sessionManager.ReportCapabilities(request.Id, request); - } - - public void Post(ReportViewing request) - { - request.SessionId = GetSession(_sessionContext).Id; - - _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId); - } - } -} From f2d7eac4189e99b587f6b7625820fb779d228ecc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 17 Jun 2020 21:08:58 +0200 Subject: [PATCH 15/25] [WIP] Move UserService to Jellyfin.Api --- Jellyfin.Api/Controllers/UserController.cs | 424 +++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 Jellyfin.Api/Controllers/UserController.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs new file mode 100644 index 0000000000..ff9373c2db --- /dev/null +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; +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; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// User controller. + /// + [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; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + 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; + } + + /// + /// Gets a list of users. + /// + /// Optional filter by IsHidden=true or false. + /// Optional filter by IsDisabled=true or false. + /// Optional filter by IsGuest=true or false. + /// + [HttpGet] + [Authorize] + public ActionResult> GetUsers( + [FromQuery] bool? isHidden, + [FromQuery] bool? isDisabled, + [FromQuery] bool? isGuest) + { + return Ok(Get(isHidden, isDisabled, isGuest, false, false)); + } + + /// + /// Gets a list of publicly visible users for display on a login screen. + /// + /// + [HttpGet("Public")] + public ActionResult> GetPublicUsers() + { + // If the startup wizard hasn't been completed then just return all users + if (!_config.Configuration.IsStartupWizardCompleted) + { + return GetUsers(null, false, null); + } + + return Ok(Get(false, false, false, true, true)); + } + + /// + /// Gets a user by Id. + /// + /// The user id. + /// + [HttpGet("{id}")] + // TODO: authorize escapeParentalControl + public ActionResult GetUserById([FromRoute] Guid id) + { + var user = _userManager.GetUserById(id); + + if (user == null) + { + throw new ResourceNotFoundException("User not found"); + } + + var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); + + return Ok(result); + } + + /// + /// Deletes a user. + /// + /// The user id. + /// A indicating success. + [HttpDelete("{id}")] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult DeleteUser([FromRoute] Guid id) + { + var user = _userManager.GetUserById(id); + + if (user == null) + { + throw new ResourceNotFoundException("User not found"); + } + + _sessionManager.RevokeUserTokens(user.Id, null); + _userManager.DeleteUser(user); + return NoContent(); + } + + /// + /// Authenticates a user. + /// + /// The user id. + /// + /// + /// + [HttpPost("{id}/Authenticate")] + public async Task> 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)) + { + throw new MethodNotAllowedException(); + } + + // Password should always be null + return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false); + } + + /// + /// Authenticates a user by name. + /// + /// The username. + /// + /// + /// + [HttpPost("AuthenticateByName")] + public async Task> AuthenticateUserByName( + [FromQuery, BindRequired] string username, + [FromQuery, BindRequired] string pw, + [FromQuery, BindRequired] string password) + { + 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, + Password = pw, + PasswordSha1 = password, + RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), + Username = username + }).ConfigureAwait(false); + + return Ok(result); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + } + } + + /// + /// Updates a user's password. + /// + /// + /// + /// + /// + /// Whether to reset the password. + /// A indicating success. + [HttpPost("{id}/Password")] + [Authorize] + public async Task UpdateUserPassword( + [FromRoute] Guid id, + [FromQuery] string currentPassword, + [FromQuery] string currentPw, + [FromQuery] string newPw, + [FromQuery] bool resetPassword) + { + AssertCanUpdateUser(_authContext, _userManager, id, true); + + 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) + { + throw new ArgumentException("Invalid user or password entered."); + } + + await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); + + var currentToken = _authContext.GetAuthorizationInfo(Request).Token; + + _sessionManager.RevokeUserTokens(user.Id, currentToken); + } + + return NoContent(); + } + + /// + /// Updates a user's easy password. + /// + /// + /// + /// + /// + /// A indicating success. + [HttpPost("{id}/EasyPassword")] + [Authorize] + public ActionResult UpdateUserEasyPassword( + [FromRoute] Guid id, + [FromQuery] string newPassword, + [FromQuery] string newPw, + [FromQuery] bool resetPassword) + { + AssertCanUpdateUser(_authContext, _userManager, id, true); + + 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(); + } + + /// + /// Updates a user. + /// + /// A indicating success. + [HttpPost("{id}")] + [Authorize] + public ActionResult UpdateUser() // TODO: missing UserDto + { + throw new NotImplementedException(); + } + + /// + /// Updates a user policy. + /// + /// The user id. + /// A indicating success. + [HttpPost("{id}/Policy")] + [Authorize] + public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy + { + throw new NotImplementedException(); + } + + /// + /// Updates a user configuration. + /// + /// The user id. + /// A indicating success. + [HttpPost("{id}/Configuration")] + [Authorize] + public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration + { + throw new NotImplementedException(); + } + + /// + /// Creates a user. + /// + /// The username. + /// The password. + /// A indicating success. + [HttpPost("/Users/New")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task CreateUserByName( + [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); + } + + /// + /// Initiates the forgot password process for a local user. + /// + /// The entered username. + /// + [HttpPost("ForgotPassword")] + public async Task> 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); + } + + /// + /// Redeems a forgot password pin. + /// + /// The pin. + /// + [HttpPost("ForgotPassword/Pin")] + public async Task> ForgotPasswordPin([FromBody] string pin) + { + var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); + return Ok(result); + } + + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork) + { + 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; + } + } +} From 713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 18 Jun 2020 18:09:58 +0200 Subject: [PATCH 16/25] Add xml comments; Add status codes; Use return instead of exception --- Jellyfin.Api/Controllers/UserController.cs | 260 ++++++-- Jellyfin.Api/Helpers/RequestHelpers.cs | 27 + .../Models/UserDtos/AuthenticateUserByName.cs | 9 + MediaBrowser.Api/UserService.cs | 605 ------------------ 4 files changed, 235 insertions(+), 666 deletions(-) create mode 100644 Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs delete mode 100644 MediaBrowser.Api/UserService.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index ff9373c2db..825219c66a 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,11 +1,15 @@ -using System; +#nullable enable +#pragma warning disable CA1801 + +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; @@ -13,9 +17,11 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -65,51 +71,60 @@ namespace Jellyfin.Api.Controllers /// Optional filter by IsHidden=true or false. /// Optional filter by IsDisabled=true or false. /// Optional filter by IsGuest=true or false. - /// + /// Users returned. + /// An containing the users. [HttpGet] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled, [FromQuery] bool? isGuest) { - return Ok(Get(isHidden, isDisabled, isGuest, false, false)); + var users = Get(isHidden, isDisabled, false, false); + return Ok(users); } /// /// Gets a list of publicly visible users for display on a login screen. /// - /// + /// Public users returned. + /// An containing the public users. [HttpGet("Public")] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return GetUsers(null, false, null); + return Ok(GetUsers(false, false, false).Value); } - return Ok(Get(false, false, false, true, true)); + return Ok(Get(false, false, true, true)); } /// /// Gets a user by Id. /// /// The user id. - /// + /// User returned. + /// User not found. + /// An with information about the user or a if the user was not found. [HttpGet("{id}")] // TODO: authorize escapeParentalControl + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); } @@ -117,16 +132,20 @@ namespace Jellyfin.Api.Controllers /// Deletes a user. /// /// The user id. - /// A indicating success. + /// User deleted. + /// User not found. + /// A indicating success or a if the user was not found. [HttpDelete("{id}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteUser([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } _sessionManager.RevokeUserTokens(user.Id, null); @@ -138,10 +157,16 @@ namespace Jellyfin.Api.Controllers /// Authenticates a user. /// /// The user id. - /// - /// - /// + /// The password as plain text. + /// The password sha1-hash. + /// User authenticated. + /// Sha1-hashed password only is not allowed. + /// User not found. + /// A containing an . [HttpPost("{id}/Authenticate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid id, [FromQuery, BindRequired] string pw, @@ -156,25 +181,22 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) { - throw new MethodNotAllowedException(); + return Forbid("Only sha1 password is not allowed."); } // Password should always be null - return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false); + return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false); } /// /// Authenticates a user by name. /// - /// The username. - /// - /// - /// + /// The request. + /// User authenticated. + /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] - public async Task> AuthenticateUserByName( - [FromQuery, BindRequired] string username, - [FromQuery, BindRequired] string pw, - [FromQuery, BindRequired] string password) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -186,10 +208,10 @@ namespace Jellyfin.Api.Controllers AppVersion = auth.Version, DeviceId = auth.DeviceId, DeviceName = auth.Device, - Password = pw, - PasswordSha1 = password, + Password = request.Pw, + PasswordSha1 = request.Password, RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), - Username = username + Username = request.Username }).ConfigureAwait(false); return Ok(result); @@ -204,22 +226,31 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's password. /// - /// - /// - /// - /// + /// The user id. + /// The current password sha1-hash. + /// The current password as plain text. + /// The new password in plain text. /// Whether to reset the password. - /// A indicating success. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/Password")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid id, - [FromQuery] string currentPassword, - [FromQuery] string currentPw, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string currentPassword, + [FromBody] string currentPw, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the password."); + } var user = _userManager.GetUserById(id); @@ -243,7 +274,7 @@ namespace Jellyfin.Api.Controllers if (success == null) { - throw new ArgumentException("Invalid user or password entered."); + return Forbid("Invalid user or password entered."); } await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); @@ -259,20 +290,29 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's easy password. /// - /// - /// - /// - /// - /// A indicating success. + /// The user id. + /// The new password sha1-hash. + /// The new password in plain text. + /// Whether to reset the password. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/EasyPassword")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid id, - [FromQuery] string newPassword, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string newPassword, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the easy password."); + } var user = _userManager.GetUserById(id); @@ -296,36 +336,128 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user. /// - /// A indicating success. + /// The user id. + /// The updated user model. + /// User updated. + /// User information was not supplied. + /// User update forbidden. + /// A indicating success or a or a on failure. [HttpPost("{id}")] [Authorize] - public ActionResult UpdateUser() // TODO: missing UserDto + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUser( + [FromRoute] Guid id, + [FromBody] UserDto updateUser) { - throw new NotImplementedException(); + 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(); } /// /// Updates a user policy. /// /// The user id. - /// A indicating success. + /// The new user policy. + /// User policy updated. + /// User policy was not supplied. + /// User policy update forbidden. + /// A indicating success or a or a on failure.. [HttpPost("{id}/Policy")] [Authorize] - public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserPolicy( + [FromRoute] Guid id, + [FromBody] UserPolicy newPolicy) { - throw new NotImplementedException(); + 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(); } /// /// Updates a user configuration. /// /// The user id. + /// The new user configuration. + /// User configuration updated. + /// User configuration update forbidden. /// A indicating success. [HttpPost("{id}/Configuration")] [Authorize] - public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserConfiguration( + [FromRoute] Guid id, + [FromBody] UserConfiguration userConfig) { - throw new NotImplementedException(); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + { + return Forbid("User configuration update not allowed"); + } + + _userManager.UpdateConfiguration(id, userConfig); + + return NoContent(); } /// @@ -333,10 +465,12 @@ namespace Jellyfin.Api.Controllers /// /// The username. /// The password. - /// A indicating success. + /// User created. + /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] - public async Task CreateUserByName( + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateUserByName( [FromBody] string name, [FromBody] string password) { @@ -357,8 +491,10 @@ namespace Jellyfin.Api.Controllers /// Initiates the forgot password process for a local user. /// /// The entered username. - /// + /// Password reset process started. + /// A containing a . [HttpPost("ForgotPassword")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPassword([FromBody] string enteredUsername) { var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) @@ -373,15 +509,17 @@ namespace Jellyfin.Api.Controllers /// Redeems a forgot password pin. /// /// The pin. - /// + /// Pin reset process started. + /// A containing a . [HttpPost("ForgotPassword/Pin")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPasswordPin([FromBody] string pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); return Ok(result); } - private IEnumerable Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork) + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 9f4d34f9c6..6d6acbcf91 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,4 +1,7 @@ using System; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers { @@ -25,5 +28,29 @@ namespace Jellyfin.Api.Helpers ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } + + /// + /// Checks if the user can update an entry. + /// + /// Instance of the interface. + /// The . + /// The user id. + /// Whether to restrict the user preferences. + /// A whether the user can update the entry. + internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) + { + var auth = authContext.GetAuthorizationInfo(requestContext); + + var authenticatedUser = auth.User; + + // If they're going to update the record of another user, they must be an administrator + if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator)) + || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess)) + { + return false; + } + + return true; + } } } diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs new file mode 100644 index 0000000000..00b90a9250 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -0,0 +1,9 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + public class AuthenticateUserByName + { + public string Username { get; set; } + public string Pw { get; set; } + public string Password { get; set; } + } +} diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs deleted file mode 100644 index 9cb9baf631..0000000000 --- a/MediaBrowser.Api/UserService.cs +++ /dev/null @@ -1,605 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; -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; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetUsers - /// - [Route("/Users", "GET", Summary = "Gets a list of users")] - [Authenticated] - public class GetUsers : IReturn - { - [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsHidden { get; set; } - - [ApiMember(Name = "IsDisabled", Description = "Optional filter by IsDisabled=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsDisabled { get; set; } - - [ApiMember(Name = "IsGuest", Description = "Optional filter by IsGuest=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsGuest { get; set; } - } - - [Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")] - public class GetPublicUsers : IReturn - { - } - - /// - /// Class GetUser - /// - [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")] - [Authenticated(EscapeParentalControl = true)] - public class GetUser : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - } - - /// - /// Class DeleteUser - /// - [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")] - [Authenticated(Roles = "Admin")] - public class DeleteUser : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - } - - /// - /// Class AuthenticateUser - /// - [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")] - public class AuthenticateUser : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - - [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pw { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - } - - /// - /// Class AuthenticateUser - /// - [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")] - public class AuthenticateUserByName : IReturn - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Username { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - - [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pw { get; set; } - } - - /// - /// Class UpdateUserPassword - /// - [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")] - [Authenticated] - public class UpdateUserPassword : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - public Guid Id { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - public string CurrentPassword { get; set; } - - public string CurrentPw { get; set; } - - public string NewPw { get; set; } - - /// - /// Gets or sets a value indicating whether [reset password]. - /// - /// true if [reset password]; otherwise, false. - public bool ResetPassword { get; set; } - } - - /// - /// Class UpdateUserEasyPassword - /// - [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")] - [Authenticated] - public class UpdateUserEasyPassword : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - public Guid Id { get; set; } - - /// - /// Gets or sets the new password. - /// - /// The new password. - public string NewPassword { get; set; } - - public string NewPw { get; set; } - - /// - /// Gets or sets a value indicating whether [reset password]. - /// - /// true if [reset password]; otherwise, false. - public bool ResetPassword { get; set; } - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}", "POST", Summary = "Updates a user")] - [Authenticated] - public class UpdateUser : UserDto, IReturnVoid - { - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")] - [Authenticated(Roles = "admin")] - public class UpdateUserPolicy : UserPolicy, IReturnVoid - { - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - } - - /// - /// Class UpdateUser - /// - [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")] - [Authenticated] - public class UpdateUserConfiguration : UserConfiguration, IReturnVoid - { - [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - } - - /// - /// Class CreateUser - /// - [Route("/Users/New", "POST", Summary = "Creates a user")] - [Authenticated(Roles = "Admin")] - public class CreateUserByName : IReturn - { - [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Name { get; set; } - - [ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Password { get; set; } - } - - [Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")] - public class ForgotPassword : IReturn - { - [ApiMember(Name = "EnteredUsername", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string EnteredUsername { get; set; } - } - - [Route("/Users/ForgotPassword/Pin", "POST", Summary = "Redeems a forgot password pin")] - public class ForgotPasswordPin : IReturn - { - [ApiMember(Name = "Pin", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")] - public string Pin { get; set; } - } - - /// - /// Class UsersService - /// - public class UserService : BaseApiService - { - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - private readonly ISessionManager _sessionMananger; - private readonly INetworkManager _networkManager; - private readonly IDeviceManager _deviceManager; - private readonly IAuthorizationContext _authContext; - - public UserService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ISessionManager sessionMananger, - INetworkManager networkManager, - IDeviceManager deviceManager, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _userManager = userManager; - _sessionMananger = sessionMananger; - _networkManager = networkManager; - _deviceManager = deviceManager; - _authContext = authContext; - } - - public object Get(GetPublicUsers request) - { - // If the startup wizard hasn't been completed then just return all users - if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - { - return Get(new GetUsers - { - IsDisabled = false - }); - } - - return Get(new GetUsers - { - IsHidden = false, - IsDisabled = false - }, true, true); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetUsers request) - { - return Get(request, false, false); - } - - private object Get(GetUsers request, bool filterByDevice, bool filterByNetwork) - { - var users = _userManager.Users; - - if (request.IsDisabled.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value); - } - - if (request.IsHidden.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.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(Request.RemoteIp)) - { - users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); - } - } - - var result = users - .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, Request.RemoteIp)) - .ToArray(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - var result = _userManager.GetUserDto(user, Request.RemoteIp); - - return ToOptimizedResult(result); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public Task Delete(DeleteUser request) - { - return DeleteAsync(request); - } - - public Task DeleteAsync(DeleteUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - _sessionMananger.RevokeUserTokens(user.Id, null); - _userManager.DeleteUser(user); - return Task.CompletedTask; - } - - /// - /// Posts the specified request. - /// - /// The request. - public object Post(AuthenticateUser request) - { - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw)) - { - throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); - } - - // Password should always be null - return Post(new AuthenticateUserByName - { - Username = user.Username, - Password = null, - Pw = request.Pw - }); - } - - public async Task Post(AuthenticateUserByName request) - { - var auth = _authContext.GetAuthorizationInfo(Request); - - try - { - var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest - { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = Request.RemoteIp, - Username = request.Username - }).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - catch (SecurityException e) - { - // rethrow adding IP address to message - throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(UpdateUserPassword request) - { - return PostAsync(request); - } - - public async Task PostAsync(UpdateUserPassword request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, true); - - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetPassword(user).ConfigureAwait(false); - } - else - { - var success = await _userManager.AuthenticateUser( - user.Username, - request.CurrentPw, - request.CurrentPassword, - Request.RemoteIp, - false).ConfigureAwait(false); - - if (success == null) - { - throw new ArgumentException("Invalid user or password entered."); - } - - await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - - _sessionMananger.RevokeUserTokens(user.Id, currentToken); - } - } - - public void Post(UpdateUserEasyPassword request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, true); - - var user = _userManager.GetUserById(request.Id); - - if (user == null) - { - throw new ResourceNotFoundException("User not found"); - } - - if (request.ResetPassword) - { - _userManager.ResetEasyPassword(user); - } - else - { - _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public async Task Post(UpdateUser request) - { - var id = Guid.Parse(GetPathValue(1)); - - AssertCanUpdateUser(_authContext, _userManager, id, false); - - var dtoUser = request; - - var user = _userManager.GetUserById(id); - - if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal)) - { - await _userManager.UpdateUserAsync(user); - _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration); - } - else - { - await _userManager.RenameUser(user, dtoUser.Name).ConfigureAwait(false); - - _userManager.UpdateConfiguration(dtoUser.Id, dtoUser.Configuration); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - /// System.Object. - public async Task Post(CreateUserByName request) - { - var newUser = _userManager.CreateUser(request.Name); - - // no need to authenticate password for new user - if (request.Password != null) - { - await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); - } - - var result = _userManager.GetUserDto(newUser, Request.RemoteIp); - - return ToOptimizedResult(result); - } - - public async Task Post(ForgotPassword request) - { - var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp); - - var result = await _userManager.StartForgotPasswordProcess(request.EnteredUsername, isLocal).ConfigureAwait(false); - - return result; - } - - public async Task Post(ForgotPasswordPin request) - { - var result = await _userManager.RedeemPasswordResetPin(request.Pin).ConfigureAwait(false); - - return result; - } - - public void Post(UpdateUserConfiguration request) - { - AssertCanUpdateUser(_authContext, _userManager, request.Id, false); - - _userManager.UpdateConfiguration(request.Id, request); - } - - public void Post(UpdateUserPolicy request) - { - var user = _userManager.GetUserById(request.Id); - - // If removing admin access - if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) - { - if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - throw new ArgumentException("There must be at least one user in the system with administrative access."); - } - } - - // If disabling - if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new ArgumentException("Administrators cannot be disabled."); - } - - // If disabling - if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) - { - if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) - { - throw new ArgumentException("There must be at least one enabled user in the system."); - } - - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - _sessionMananger.RevokeUserTokens(user.Id, currentToken); - } - - _userManager.UpdatePolicy(request.Id, request); - } - } -} From 77bea567082528be3d1da09ed214ec0a1e192a97 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 18 Jun 2020 19:35:29 +0200 Subject: [PATCH 17/25] Add request body models --- Jellyfin.Api/Controllers/UserController.cs | 54 ++++++++----------- .../Models/UserDtos/AuthenticateUserByName.cs | 20 +++++-- .../Models/UserDtos/CreateUserByName.cs | 18 +++++++ .../Models/UserDtos/UpdateUserEasyPassword.cs | 23 ++++++++ .../Models/UserDtos/UpdateUserPassword.cs | 28 ++++++++++ 5 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 Jellyfin.Api/Models/UserDtos/CreateUserByName.cs create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 825219c66a..24123085bf 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -111,8 +111,7 @@ namespace Jellyfin.Api.Controllers /// User not found. /// An with information about the user or a if the user was not found. [HttpGet("{id}")] - // TODO: authorize escapeParentalControl - [Authorize] + [Authorize(Policy = Policies.IgnoreSchedule)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid id) @@ -185,7 +184,13 @@ namespace Jellyfin.Api.Controllers } // Password should always be null - return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false); + AuthenticateUserByName request = new AuthenticateUserByName + { + Username = user.Username, + Password = null, + Pw = pw + }; + return await AuthenticateUserByName(request).ConfigureAwait(false); } /// @@ -227,10 +232,7 @@ namespace Jellyfin.Api.Controllers /// Updates a user's password. /// /// The user id. - /// The current password sha1-hash. - /// The current password as plain text. - /// The new password in plain text. - /// Whether to reset the password. + /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. @@ -242,10 +244,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid id, - [FromBody] string currentPassword, - [FromBody] string currentPw, - [FromBody] string newPw, - [FromBody] bool resetPassword) + [FromBody] UpdateUserPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) { @@ -259,7 +258,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (resetPassword) + if (request.ResetPassword) { await _userManager.ResetPassword(user).ConfigureAwait(false); } @@ -267,8 +266,8 @@ namespace Jellyfin.Api.Controllers { var success = await _userManager.AuthenticateUser( user.Username, - currentPw, - currentPassword, + request.CurrentPw, + request.CurrentPw, HttpContext.Connection.RemoteIpAddress.ToString(), false).ConfigureAwait(false); @@ -277,7 +276,7 @@ namespace Jellyfin.Api.Controllers return Forbid("Invalid user or password entered."); } - await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); + await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); var currentToken = _authContext.GetAuthorizationInfo(Request).Token; @@ -291,9 +290,7 @@ namespace Jellyfin.Api.Controllers /// Updates a user's easy password. /// /// The user id. - /// The new password sha1-hash. - /// The new password in plain text. - /// Whether to reset the password. + /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. @@ -305,9 +302,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid id, - [FromBody] string newPassword, - [FromBody] string newPw, - [FromBody] bool resetPassword) + [FromBody] UpdateUserEasyPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) { @@ -321,13 +316,13 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (resetPassword) + if (request.ResetPassword) { _userManager.ResetEasyPassword(user); } else { - _userManager.ChangeEasyPassword(user, newPw, newPassword); + _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); } return NoContent(); @@ -463,23 +458,20 @@ namespace Jellyfin.Api.Controllers /// /// Creates a user. /// - /// The username. - /// The password. + /// The create user by name request body. /// User created. /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateUserByName( - [FromBody] string name, - [FromBody] string password) + public async Task> CreateUserByName([FromBody] CreateUserByName request) { - var newUser = _userManager.CreateUser(name); + var newUser = _userManager.CreateUser(request.Name); // no need to authenticate password for new user - if (password != null) + if (request.Password != null) { - await _userManager.ChangePassword(newUser, password).ConfigureAwait(false); + await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs index 00b90a9250..3936274356 100644 --- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -1,9 +1,23 @@ namespace Jellyfin.Api.Models.UserDtos { + /// + /// The authenticate user by name request body. + /// public class AuthenticateUserByName { - public string Username { get; set; } - public string Pw { get; set; } - public string Password { get; set; } + /// + /// Gets or sets the username. + /// + public string? Username { get; set; } + + /// + /// Gets or sets the plain text password. + /// + public string? Pw { get; set; } + + /// + /// Gets or sets the sha1-hashed password. + /// + public string? Password { get; set; } } } diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs new file mode 100644 index 0000000000..1c88d36287 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The create user by name request body. + /// + public class CreateUserByName + { + /// + /// Gets or sets the username. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the password. + /// + public string? Password { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs new file mode 100644 index 0000000000..0a173ea1a9 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs @@ -0,0 +1,23 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The update user easy password request body. + /// + public class UpdateUserEasyPassword + { + /// + /// Gets or sets the new sha1-hashed password. + /// + public string? NewPassword { get; set; } + + /// + /// Gets or sets the new password. + /// + public string? NewPw { get; set; } + + /// + /// Gets or sets a value indicating whether to reset the password. + /// + public bool ResetPassword { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs new file mode 100644 index 0000000000..8288dbbc44 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs @@ -0,0 +1,28 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The update user password request body. + /// + public class UpdateUserPassword + { + /// + /// Gets or sets the current sha1-hashed password. + /// + public string? CurrentPassword { get; set; } + + /// + /// Gets or sets the current plain text password. + /// + public string? CurrentPw { get; set; } + + /// + /// Gets or sets the new plain text password. + /// + public string? NewPw { get; set; } + + /// + /// Gets or sets a value indicating whether to reset the password. + /// + public bool ResetPassword { get; set; } + } +} From 6651cb8d24f0de690b3be68db7c0b78e2413534f Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 12:24:39 +0200 Subject: [PATCH 18/25] Add JsonInto32Converter Add additional swagger type mapping --- .../ApiServiceCollectionExtensions.cs | 13 +++++++ .../Json/Converters/JsonInt32Converter.cs | 39 +++++++------------ MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dbd5ba4166..821a52e476 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -215,6 +215,19 @@ namespace Jellyfin.Server.Extensions Format = "string" }) }); + + options.MapType>>(() => + new OpenApiSchema + { + Type = "object", + Properties = typeof(ImageType).GetEnumNames().ToDictionary( + name => name, + name => new OpenApiSchema + { + Type = "string", + Format = "string" + }) + }); } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs index fe5dd6cd4d..70c375b8cd 100644 --- a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs @@ -14,40 +14,27 @@ namespace MediaBrowser.Common.Json.Converters /// public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - static void ThrowFormatException() => throw new FormatException("Invalid format for an integer."); - ReadOnlySpan span = stackalloc byte[0]; + if (reader.TokenType == JsonTokenType.String) + { + ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed) + { + return number; + } - if (reader.HasValueSequence) - { - long sequenceLength = reader.ValueSequence.Length; - Span stackSpan = stackalloc byte[(int)sequenceLength]; - reader.ValueSequence.CopyTo(stackSpan); - span = stackSpan; - } - else - { - span = reader.ValueSpan; + if (int.TryParse(reader.GetString(), out number)) + { + return number; + } } - if (!Utf8Parser.TryParse(span, out int number, out _)) - { - ThrowFormatException(); - } - - return number; + return reader.GetInt32(); } /// public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) { - static void ThrowInvalidOperationException() => throw new InvalidOperationException(); - Span span = stackalloc byte[16]; - if (Utf8Formatter.TryFormat(value, span, out int bytesWritten)) - { - writer.WriteStringValue(span.Slice(0, bytesWritten)); - } - - ThrowInvalidOperationException(); + writer.WriteNumberValue(value); } } } diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index adc15123b1..ec3c45476c 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -28,6 +28,7 @@ namespace MediaBrowser.Common.Json }; options.Converters.Add(new JsonGuidConverter()); + options.Converters.Add(new JsonInt32Converter()); options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); From a5bd7f2d6ee0fef67c34f61db9be36167c30d890 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 13:03:53 +0200 Subject: [PATCH 19/25] Use new authorization and session functions --- Jellyfin.Api/Controllers/SessionController.cs | 32 ++++++++----------- Jellyfin.Api/Helpers/RequestHelpers.cs | 14 ++++++-- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 5b60275eb6..4f259536a1 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Helpers; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -27,7 +28,6 @@ namespace Jellyfin.Api.Controllers private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; private readonly IDeviceManager _deviceManager; - private readonly ISessionContext _sessionContext; /// /// Initializes a new instance of the class. @@ -36,19 +36,16 @@ namespace Jellyfin.Api.Controllers /// Instance of interface. /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SessionController( ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, - IDeviceManager deviceManager, - ISessionContext sessionContext) + IDeviceManager deviceManager) { _sessionManager = sessionManager; _userManager = userManager; _authContext = authContext; _deviceManager = deviceManager; - _sessionContext = sessionContext; } /// @@ -80,12 +77,12 @@ namespace Jellyfin.Api.Controllers var user = _userManager.GetUserById(controllableByUserId); - if (!user.Policy.EnableRemoteControlOfOtherUsers) + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId)); } - if (!user.Policy.EnableSharedDeviceControl) + if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { result = result.Where(i => !i.UserId.Equals(Guid.Empty)); } @@ -138,7 +135,7 @@ namespace Jellyfin.Api.Controllers }; _sessionManager.SendBrowseCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None); @@ -175,7 +172,7 @@ namespace Jellyfin.Api.Controllers playRequest.PlayCommand = playCommand; _sessionManager.SendPlayCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, playRequest, CancellationToken.None); @@ -197,7 +194,7 @@ namespace Jellyfin.Api.Controllers [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( - RequestHelpers.GetSession(_sessionContext).Id, + RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, playstateRequest, CancellationToken.None); @@ -224,7 +221,7 @@ namespace Jellyfin.Api.Controllers name = commandType.ToString(); } - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); var generalCommand = new GeneralCommand { Name = name, @@ -249,7 +246,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] string id, [FromRoute] string command) { - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); var generalCommand = new GeneralCommand { @@ -275,7 +272,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] string id, [FromBody, Required] GeneralCommand command) { - var currentSession = RequestHelpers.GetSession(_sessionContext); + var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); if (command == null) { @@ -317,7 +314,7 @@ namespace Jellyfin.Api.Controllers Text = text }; - _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None); + _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None); return NoContent(); } @@ -379,7 +376,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionContext).Id; + id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; } _sessionManager.ReportCapabilities(id, new ClientCapabilities @@ -408,7 +405,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionContext).Id; + id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; } _sessionManager.ReportCapabilities(id, capabilities); @@ -429,7 +426,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string sessionId, [FromQuery] string itemId) { - string session = RequestHelpers.GetSession(_sessionContext).Id; + string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); @@ -444,7 +441,6 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportSessionEnded() { - // TODO: how do we get AuthorizationInfo without an IRequest? AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); _sessionManager.Logout(auth.Token); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index ae8ab37e8e..2aa700de3b 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,6 +1,7 @@ using System; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers { @@ -28,10 +29,17 @@ namespace Jellyfin.Api.Helpers : value.Split(separator); } - internal static SessionInfo GetSession(ISessionContext sessionContext) + internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) { - // TODO: how do we get a SessionInfo without IRequest? - SessionInfo session = sessionContext.GetSession("Request"); + var authorization = authContext.GetAuthorizationInfo(request); + var user = authorization.User; + var session = sessionManager.LogSessionActivity( + authorization.Client, + authorization.Version, + authorization.DeviceId, + authorization.Device, + request.HttpContext.Connection.RemoteIpAddress.ToString(), + user); if (session == null) { From 08401f923dbe1bdf0086f905a6a9575b5ac9c5cd Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 15:49:44 +0200 Subject: [PATCH 20/25] Change swagger dictionary type mapping --- .../Extensions/ApiServiceCollectionExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 821a52e476..aad61d0429 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -216,6 +216,9 @@ namespace Jellyfin.Server.Extensions }) }); + /* + * Support BlurHash dictionary + */ options.MapType>>(() => new OpenApiSchema { @@ -224,8 +227,17 @@ namespace Jellyfin.Server.Extensions name => name, name => new OpenApiSchema { - Type = "string", - Format = "string" + Type = "object", Properties = new Dictionary + { + { + "string", + new OpenApiSchema + { + Type = "string", + Format = "string" + } + } + } }) }); } From d820c0fff50a79d85349660f507e820704d80d45 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 08:49:42 -0600 Subject: [PATCH 21/25] Convert pragma to supresswarning --- Jellyfin.Api/Controllers/ActivityLogController.cs | 4 ++-- Jellyfin.Api/Controllers/FilterController.cs | 6 +++--- Jellyfin.Api/Controllers/ItemRefreshController.cs | 4 ++-- .../Controllers/LibraryStructureController.cs | 4 ++-- Jellyfin.Api/Controllers/NotificationsController.cs | 12 ++++++++++-- Jellyfin.Api/Controllers/PluginsController.cs | 6 +++--- Jellyfin.Api/Controllers/SubtitleController.cs | 5 ++--- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 4ae7cf5069..ec50fb022e 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; @@ -41,6 +40,7 @@ namespace Jellyfin.Api.Controllers /// A containing the log entries. [HttpGet("Entries")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")] public ActionResult> GetLogEntries( [FromQuery] int? startIndex, [FromQuery] int? limit, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 6a6e6a64a3..dc5b0d9061 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - -using System; +using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -137,6 +136,7 @@ namespace Jellyfin.Api.Controllers /// Query filters. [HttpGet("/Items/Filters2")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")] public ActionResult GetQueryFilters( [FromQuery] Guid? userId, [FromQuery] string? parentId, diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index a1df22e411..6a16a89c5a 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,6 +1,5 @@ -#pragma warning disable CA1801 - using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; @@ -54,6 +53,7 @@ namespace Jellyfin.Api.Controllers [Description("Refreshes metadata for an item.")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")] public ActionResult Post( [FromRoute] string id, [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ca2905b114..62c5474099 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -56,6 +55,7 @@ namespace Jellyfin.Api.Controllers /// An with the virtual folders. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetVirtualFolders([FromQuery] string userId) { return _libraryManager.GetVirtualFolders(true); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index a1f9b9e8f7..01dd23c77f 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Jellyfin.Api.Models.NotificationDtos; @@ -45,6 +44,10 @@ namespace Jellyfin.Api.Controllers /// An containing a list of notifications. [HttpGet("{UserID}")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] public ActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, @@ -62,6 +65,7 @@ namespace Jellyfin.Api.Controllers /// An containing a summary of the users notifications. [HttpGet("{UserID}/Summary")] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult GetNotificationsSummary( [FromRoute] string userId) { @@ -136,6 +140,8 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] public ActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) @@ -152,6 +158,8 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] public ActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index fdb2f4c35b..6075544cf7 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1801 - -using System; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -46,6 +45,7 @@ namespace Jellyfin.Api.Controllers /// Installed plugins returned. /// List of currently installed plugins. [HttpGet] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")] public ActionResult> GetPlugins([FromRoute] bool? isAppStoreEnabled) { return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 69b83379d9..74ec5f9b52 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,8 +1,7 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -251,9 +250,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task GetSubtitlePlaylist( [FromRoute] Guid id, - // TODO: 'int index' is never used: CA1801 is disabled [FromRoute] int index, [FromRoute] string mediaSourceId, [FromQuery, Required] int segmentLength) From d53a2899b1a82af1c64f3a2a558ae1ef201905ec Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 09:03:04 -0600 Subject: [PATCH 22/25] remove #nullable --- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 35efe6b5f8..697a0baf42 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.ComponentModel.DataAnnotations; using System.Threading; using MediaBrowser.Controller.Persistence; From 68ea589f1af5bc5fdc656013d6e5d1deec99341f Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Fri, 19 Jun 2020 18:11:46 +0200 Subject: [PATCH 23/25] Use direct return instead of Ok() --- Jellyfin.Api/Controllers/UserController.cs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 24123085bf..68ab5813ce 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,9 +1,7 @@ -#nullable enable -#pragma warning disable CA1801 - -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -76,6 +74,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled, @@ -97,7 +96,7 @@ namespace Jellyfin.Api.Controllers // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return Ok(GetUsers(false, false, false).Value); + return Ok(Get(false, false, false, false)); } return Ok(Get(false, false, true, true)); @@ -124,7 +123,7 @@ namespace Jellyfin.Api.Controllers } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); + return result; } /// @@ -219,7 +218,7 @@ namespace Jellyfin.Api.Controllers Username = request.Username }).ConfigureAwait(false); - return Ok(result); + return result; } catch (SecurityException e) { @@ -476,7 +475,7 @@ namespace Jellyfin.Api.Controllers var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); + return result; } /// @@ -494,7 +493,7 @@ namespace Jellyfin.Api.Controllers var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); - return Ok(result); + return result; } /// @@ -508,7 +507,7 @@ namespace Jellyfin.Api.Controllers public async Task> ForgotPasswordPin([FromBody] string pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); - return Ok(result); + return result; } private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) @@ -545,8 +544,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())) - .ToArray(); + .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())); return result; } From f17d198eb5643f551973b1e54405a472fe0b55b2 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 22:18:46 +0200 Subject: [PATCH 24/25] Move SuggestionsService to Jellyfin.Api --- .../Controllers/SuggestionsController.cs | 86 ++++++++++++++++ MediaBrowser.Api/SuggestionsService.cs | 98 ------------------- 2 files changed, 86 insertions(+), 98 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SuggestionsController.cs delete mode 100644 MediaBrowser.Api/SuggestionsService.cs diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs new file mode 100644 index 0000000000..2d6445c305 --- /dev/null +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Jellyfin.Api.Extensions; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The suggestions controller. + /// + public class SuggestionsController : BaseJellyfinApiController + { + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SuggestionsController( + IDtoService dtoService, + IUserManager userManager, + ILibraryManager libraryManager) + { + _dtoService = dtoService; + _userManager = userManager; + _libraryManager = libraryManager; + } + + /// + /// Gets suggestions. + /// + /// The user id. + /// The media types. + /// The type. + /// Whether to enable the total record count. + /// Optional. The start index. + /// Optional. The limit. + /// Suggestions returned. + /// A with the suggestions. + [HttpGet("/Users/{userId}/Suggestions")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSuggestions( + [FromRoute] Guid userId, + [FromQuery] string? mediaType, + [FromQuery] string? type, + [FromQuery] bool enableTotalRecordCount, + [FromQuery] int? startIndex, + [FromQuery] int? limit) + { + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var dtoOptions = new DtoOptions().AddClientFields(Request); + var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), + MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + IsVirtualItem = false, + StartIndex = startIndex, + Limit = limit, + DtoOptions = dtoOptions, + EnableTotalRecordCount = enableTotalRecordCount, + Recursive = true + }); + + var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); + + return new QueryResult + { + TotalRecordCount = result.TotalRecordCount, + Items = dtoList + }; + } + } +} diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs deleted file mode 100644 index 32d3bde5cb..0000000000 --- a/MediaBrowser.Api/SuggestionsService.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Linq; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Users/{UserId}/Suggestions", "GET", Summary = "Gets items based on a query.")] - public class GetSuggestedItems : IReturn> - { - public string MediaType { get; set; } - public string Type { get; set; } - public Guid UserId { get; set; } - public bool EnableTotalRecordCount { get; set; } - public int? StartIndex { get; set; } - public int? Limit { get; set; } - - public string[] GetMediaTypes() - { - return (MediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public string[] GetIncludeItemTypes() - { - return (Type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - } - - public class SuggestionsService : BaseApiService - { - private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - - public SuggestionsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDtoService dtoService, - IAuthorizationContext authContext, - IUserManager userManager, - ILibraryManager libraryManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _dtoService = dtoService; - _authContext = authContext; - _userManager = userManager; - _libraryManager = libraryManager; - } - - public object Get(GetSuggestedItems request) - { - return GetResultItems(request); - } - - private QueryResult GetResultItems(GetSuggestedItems request) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var dtoOptions = GetDtoOptions(_authContext, request); - var result = GetItems(request, user, dtoOptions); - - var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - - return new QueryResult - { - TotalRecordCount = result.TotalRecordCount, - Items = dtoList - }; - } - - private QueryResult GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions) - { - return _libraryManager.GetItemsResult(new InternalItemsQuery(user) - { - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - MediaTypes = request.GetMediaTypes(), - IncludeItemTypes = request.GetIncludeItemTypes(), - IsVirtualItem = false, - StartIndex = request.StartIndex, - Limit = request.Limit, - DtoOptions = dtoOptions, - EnableTotalRecordCount = request.EnableTotalRecordCount, - Recursive = true - }); - } - } -} From 33de0ac10880a941a10f28c64f18253cc711b8da Mon Sep 17 00:00:00 2001 From: David Date: Sat, 20 Jun 2020 12:10:45 +0200 Subject: [PATCH 25/25] Use RequestHelpers.Split --- Jellyfin.Api/Controllers/SuggestionsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 2d6445c305..e1a99a1385 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -64,8 +65,8 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + MediaTypes = RequestHelpers.Split(mediaType!, ',', true), + IncludeItemTypes = RequestHelpers.Split(type!, ',', true), IsVirtualItem = false, StartIndex = startIndex, Limit = limit,