diff options
| author | David <daullmer@gmail.com> | 2020-06-18 18:52:01 +0200 |
|---|---|---|
| committer | David <daullmer@gmail.com> | 2020-06-18 18:52:01 +0200 |
| commit | 762eeb51e66368e5f6fdede7b6fab84ea5859107 (patch) | |
| tree | c9d18360013b149f204e0d6e4e60292ce45d54f4 /Jellyfin.Api | |
| parent | 713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b (diff) | |
| parent | 522e44de59a8661a859f6a373e495a9e0e8d13ff (diff) | |
Merge remote-tracking branch 'remotes/upstream/api-migration' into api-user
Diffstat (limited to 'Jellyfin.Api')
35 files changed, 811 insertions, 70 deletions
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs new file mode 100644 index 000000000..b5b9d8904 --- /dev/null +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -0,0 +1,102 @@ +#nullable enable + +using System.Net; +using System.Security.Claims; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth +{ + /// <summary> + /// Base authorization handler. + /// </summary> + /// <typeparam name="T">Type of Authorization Requirement.</typeparam> + public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T> + where T : IAuthorizationRequirement + { + private readonly IUserManager _userManager; + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// <summary> + /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class. + /// </summary> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + protected BaseAuthorizationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + { + _userManager = userManager; + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; + } + + /// <summary> + /// Validate authenticated claims. + /// </summary> + /// <param name="claimsPrincipal">Request claims.</param> + /// <param name="ignoreSchedule">Whether to ignore parental control.</param> + /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param> + /// <returns>Validated claim status.</returns> + protected bool ValidateClaims( + ClaimsPrincipal claimsPrincipal, + bool ignoreSchedule = false, + bool localAccessOnly = false) + { + // Ensure claim has userId. + var userId = ClaimHelpers.GetUserId(claimsPrincipal); + if (userId == null) + { + return false; + } + + // Ensure userId links to a valid user. + var user = _userManager.GetUserById(userId.Value); + if (user == null) + { + return false; + } + + // Ensure user is not disabled. + if (user.HasPermission(PermissionKind.IsDisabled)) + { + return false; + } + + var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString(); + var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip); + // User cannot access remotely and user is remote + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) + { + return false; + } + + if (localAccessOnly && !isInLocalNetwork) + { + return false; + } + + // User attempting to access out of parental control hours. + if (!ignoreSchedule + && !user.HasPermission(PermissionKind.IsAdministrator) + && !user.IsParentalScheduleAllowed()) + { + return false; + } + + return true; + } + + private static IPAddress NormalizeIp(IPAddress ip) + { + return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip; + } + } +} diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index a5c4e9974..5e5e25e84 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,3 +1,6 @@ +#nullable enable + +using System.Globalization; using System.Security.Authentication; using System.Security.Claims; using System.Text.Encodings.Web; @@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth /// <inheritdoc /> protected override Task<AuthenticateResult> HandleAuthenticateAsync() { - var authenticatedAttribute = new AuthenticatedAttribute - { - IgnoreLegacyAuth = true - }; - try { - var user = _authService.Authenticate(Request, authenticatedAttribute); - if (user == null) + var authorizationInfo = _authService.Authenticate(Request); + if (authorizationInfo == null) { return Task.FromResult(AuthenticateResult.NoResult()); // TODO return when legacy API is removed. @@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth var claims = new[] { - new Claim(ClaimTypes.Name, user.Username), - new Claim( - ClaimTypes.Role, - value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User) + new Claim(ClaimTypes.Name, authorizationInfo.User.Username), + new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User), + new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId), + new Claim(InternalClaimTypes.Device, authorizationInfo.Device), + new Claim(InternalClaimTypes.Client, authorizationInfo.Client), + new Claim(InternalClaimTypes.Version, authorizationInfo.Version), + new Claim(InternalClaimTypes.Token, authorizationInfo.Token), }; + var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs new file mode 100644 index 000000000..b5913daab --- /dev/null +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy +{ + /// <summary> + /// Default authorization handler. + /// </summary> + public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement> + { + /// <summary> + /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class. + /// </summary> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public DefaultAuthorizationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// <inheritdoc /> + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) + { + var validated = ValidateClaims(context.User); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs new file mode 100644 index 000000000..7cea00b69 --- /dev/null +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy +{ + /// <summary> + /// The default authorization requirement. + /// </summary> + public class DefaultAuthorizationRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index 34aa5d12c..decbe0c03 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,22 +1,33 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy { /// <summary> /// Authorization handler for requiring first time setup or elevated privileges. /// </summary> - public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement> + public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement> { private readonly IConfigurationManager _configurationManager; /// <summary> /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. /// </summary> - /// <param name="configurationManager">The jellyfin configuration manager.</param> - public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager) + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public FirstTimeSetupOrElevatedHandler( + IConfigurationManager configurationManager, + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) { _configurationManager = configurationManager; } @@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { context.Succeed(firstTimeSetupOrElevatedRequirement); + return Task.CompletedTask; } - else if (context.User.IsInRole(UserRoles.Administrator)) + + var validated = ValidateClaims(context.User); + if (validated && context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(firstTimeSetupOrElevatedRequirement); } diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs new file mode 100644 index 000000000..9afa0b28f --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy +{ + /// <summary> + /// Escape schedule controls handler. + /// </summary> + public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement> + { + /// <summary> + /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class. + /// </summary> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public IgnoreScheduleHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// <inheritdoc /> + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs new file mode 100644 index 000000000..d5bb61ce6 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy +{ + /// <summary> + /// Escape schedule controls requirement. + /// </summary> + public class IgnoreScheduleRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs new file mode 100644 index 000000000..af73352bc --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.LocalAccessPolicy +{ + /// <summary> + /// Local access handler. + /// </summary> + public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement> + { + /// <summary> + /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class. + /// </summary> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public LocalAccessHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// <inheritdoc /> + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) + { + var validated = ValidateClaims(context.User, localAccessOnly: true); + if (!validated) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs new file mode 100644 index 000000000..761127fa4 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.LocalAccessPolicy +{ + /// <summary> + /// The local access authorization requirement. + /// </summary> + public class LocalAccessRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index 2d3bb1aa4..b235c4b63 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,21 +1,43 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.RequiresElevationPolicy { /// <summary> /// Authorization handler for requiring elevated privileges. /// </summary> - public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement> + public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement> { + /// <summary> + /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class. + /// </summary> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public RequiresElevationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + /// <inheritdoc /> protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) { - if (context.User.IsInRole(UserRoles.Administrator)) + var validated = ValidateClaims(context.User); + if (validated && context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); } + else + { + context.Fail(); + } return Task.CompletedTask; } diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs new file mode 100644 index 000000000..4d7c7135d --- /dev/null +++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs @@ -0,0 +1,38 @@ +namespace Jellyfin.Api.Constants +{ + /// <summary> + /// Internal claim types for authorization. + /// </summary> + public static class InternalClaimTypes + { + /// <summary> + /// User Id. + /// </summary> + public const string UserId = "Jellyfin-UserId"; + + /// <summary> + /// Device Id. + /// </summary> + public const string DeviceId = "Jellyfin-DeviceId"; + + /// <summary> + /// Device. + /// </summary> + public const string Device = "Jellyfin-Device"; + + /// <summary> + /// Client. + /// </summary> + public const string Client = "Jellyfin-Client"; + + /// <summary> + /// Version. + /// </summary> + public const string Version = "Jellyfin-Version"; + + /// <summary> + /// Token. + /// </summary> + public const string Token = "Jellyfin-Token"; + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index e2b383f75..cf574e43d 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -6,6 +6,11 @@ namespace Jellyfin.Api.Constants public static class Policies { /// <summary> + /// Policy name for default authorization. + /// </summary> + public const string DefaultAuthorization = "DefaultAuthorization"; + + /// <summary> /// Policy name for requiring first time setup or elevated privileges. /// </summary> public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated"; @@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants /// Policy name for requiring elevated privileges. /// </summary> public const string RequiresElevation = "RequiresElevation"; + + /// <summary> + /// Policy name for allowing local access only. + /// </summary> + public const string LocalAccessOnly = "LocalAccessOnly"; + + /// <summary> + /// Policy name for escaping schedule controls. + /// </summary> + public const string IgnoreSchedule = "IgnoreSchedule"; } } diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 895d9f719..4ae7cf506 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 780a38aa8..74f1677bd 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// Configuration Controller. /// </summary> [Route("System")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 1754b0cbd..78368eed6 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; @@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Devices Controller. /// </summary> - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 46911ce93..6a6e6a64a 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Linq; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index fa46b6dd1..70f46ffa4 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index d9b8357d2..a1df22e41 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System.ComponentModel; diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs new file mode 100644 index 000000000..ca2905b11 --- /dev/null +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -0,0 +1,341 @@ +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// The library structure controller. + /// </summary> + [Route("/Library/VirtualFolders")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + public class LibraryStructureController : BaseJellyfinApiController + { + private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; + private readonly ILibraryMonitor _libraryMonitor; + + /// <summary> + /// Initializes a new instance of the <see cref="LibraryStructureController"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> + /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param> + public LibraryStructureController( + IServerConfigurationManager serverConfigurationManager, + ILibraryManager libraryManager, + ILibraryMonitor libraryMonitor) + { + _appPaths = serverConfigurationManager.ApplicationPaths; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + } + + /// <summary> + /// Gets all virtual folders. + /// </summary> + /// <param name="userId">The user id.</param> + /// <response code="200">Virtual folders retrieved.</response> + /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns> + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId) + { + return _libraryManager.GetVirtualFolders(true); + } + + /// <summary> + /// Adds a virtual folder. + /// </summary> + /// <param name="name">The name of the virtual folder.</param> + /// <param name="collectionType">The type of the collection.</param> + /// <param name="refreshLibrary">Whether to refresh the library.</param> + /// <param name="paths">The paths of the virtual folder.</param> + /// <param name="libraryOptions">The library options.</param> + /// <response code="204">Folder added.</response> + /// <returns>A <see cref="NoContentResult"/>.</returns> + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task<ActionResult> AddVirtualFolder( + [FromQuery] string name, + [FromQuery] string collectionType, + [FromQuery] bool refreshLibrary, + [FromQuery] string[] paths, + [FromQuery] LibraryOptions libraryOptions) + { + libraryOptions ??= new LibraryOptions(); + + if (paths != null && paths.Length > 0) + { + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); + } + + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); + + return NoContent(); + } + + /// <summary> + /// Removes a virtual folder. + /// </summary> + /// <param name="name">The name of the folder.</param> + /// <param name="refreshLibrary">Whether to refresh the library.</param> + /// <response code="204">Folder removed.</response> + /// <returns>A <see cref="NoContentResult"/>.</returns> + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task<ActionResult> RemoveVirtualFolder( + [FromQuery] string name, + [FromQuery] bool refreshLibrary) + { + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); + return NoContent(); + } + + /// <summary> + /// Renames a virtual folder. + /// </summary> + /// <param name="name">The name of the virtual folder.</param> + /// <param name="newName">The new name.</param> + /// <param name="refreshLibrary">Whether to refresh the library.</param> + /// <response code="204">Folder renamed.</response> + /// <response code="404">Library doesn't exist.</response> + /// <response code="409">Library already exists.</response> + /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns> + /// <exception cref="ArgumentNullException">The new name may not be null.</exception> + [HttpPost("Name")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public ActionResult RenameVirtualFolder( + [FromQuery] string name, + [FromQuery] string newName, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + var rootFolderPath = _appPaths.DefaultUserViewsPath; + + var currentPath = Path.Combine(rootFolderPath, name); + var newPath = Path.Combine(rootFolderPath, newName); + + if (!Directory.Exists(currentPath)) + { + return NotFound("The media collection does not exist."); + } + + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) + { + return Conflict($"The media library already exists at {newPath}."); + } + + _libraryMonitor.Stop(); + + try + { + // Changing capitalization. Handle windows case insensitivity + if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) + { + var tempPath = Path.Combine( + rootFolderPath, + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + Directory.Move(currentPath, tempPath); + currentPath = tempPath; + } + + Directory.Move(currentPath, newPath); + } + finally + { + CollectionFolder.OnCollectionFolderChange(); + + Task.Run(async () => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// <summary> + /// Add a media path to a library. + /// </summary> + /// <param name="name">The name of the library.</param> + /// <param name="path">The path to add.</param> + /// <param name="pathInfo">The path info.</param> + /// <param name="refreshLibrary">Whether to refresh the library.</param> + /// <returns>A <see cref="NoContentResult"/>.</returns> + /// <response code="204">Media path added.</response> + /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> + [HttpPost("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] MediaPathInfo pathInfo, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + var mediaPath = pathInfo ?? new MediaPathInfo { Path = path }; + + _libraryManager.AddMediaPath(name, mediaPath); + } + finally + { + Task.Run(async () => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// <summary> + /// Updates a media path. + /// </summary> + /// <param name="name">The name of the library.</param> + /// <param name="pathInfo">The path info.</param> + /// <returns>A <see cref="NoContentResult"/>.</returns> + /// <response code="204">Media path updated.</response> + /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> + [HttpPost("Paths/Update")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateMediaPath( + [FromQuery] string name, + [FromQuery] MediaPathInfo pathInfo) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryManager.UpdateMediaPath(name, pathInfo); + return NoContent(); + } + + /// <summary> + /// Remove a media path. + /// </summary> + /// <param name="name">The name of the library.</param> + /// <param name="path">The path to remove.</param> + /// <param name="refreshLibrary">Whether to refresh the library.</param> + /// <returns>A <see cref="NoContentResult"/>.</returns> + /// <response code="204">Media path removed.</response> + /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> + [HttpDelete("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + _libraryManager.RemoveMediaPath(name, path); + } + finally + { + Task.Run(async () => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// <summary> + /// Update library options. + /// </summary> + /// <param name="id">The library name.</param> + /// <param name="libraryOptions">The library options.</param> + /// <response code="204">Library updated.</response> + /// <returns>A <see cref="NoContentResult"/>.</returns> + [HttpPost("LibraryOptions")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateLibraryOptions( + [FromQuery] string id, + [FromQuery] LibraryOptions libraryOptions) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); + + collectionFolder.UpdateLibraryOptions(libraryOptions); + return NoContent(); + } + } +} diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index a76675d5a..a1f9b9e8f 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 8200f891c..943c23f8e 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// Package Controller. /// </summary> [Route("Packages")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class PackageController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 59196a41a..fdb2f4c35 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Collections.Generic; diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 7c5f17e9e..80983ee64 100644 --- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; @@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Jellyfin.Api.Controllers.Images +namespace Jellyfin.Api.Controllers { /// <summary> /// Remote Images Controller. diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index ec05e4fb4..d971889db 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers /// Search controller. /// </summary> [Route("/Search/Hints")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class SearchController : BaseJellyfinApiController { private readonly ISearchEngine _searchEngine; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 97df8c60d..69b83379d 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1801 using System; @@ -110,7 +109,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Subtitles retrieved.</response> /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles( [FromRoute] Guid id, @@ -130,7 +129,7 @@ namespace Jellyfin.Api.Controllers /// <response code="204">Subtitle downloaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> DownloadRemoteSubtitles( [FromRoute] Guid id, @@ -160,7 +159,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">File returned.</response> /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> [HttpGet("/Providers/Subtitles/Subtitles/{id}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id) @@ -250,7 +249,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Subtitle playlist retrieved.</response> /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns> [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult> GetSubtitlePlaylist( [FromRoute] Guid id, diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 86d9322fe..2528fd75d 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,9 +1,8 @@ -#nullable enable - using System; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// </summary> [Route("Videos")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs new file mode 100644 index 000000000..a07d4ed82 --- /dev/null +++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs @@ -0,0 +1,77 @@ +#nullable enable + +using System; +using System.Linq; +using System.Security.Claims; +using Jellyfin.Api.Constants; + +namespace Jellyfin.Api.Helpers +{ + /// <summary> + /// Claim Helpers. + /// </summary> + public static class ClaimHelpers + { + /// <summary> + /// Get user id from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>User id.</returns> + public static Guid? GetUserId(in ClaimsPrincipal user) + { + var value = GetClaimValue(user, InternalClaimTypes.UserId); + return string.IsNullOrEmpty(value) + ? null + : (Guid?)Guid.Parse(value); + } + + /// <summary> + /// Get device id from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Device id.</returns> + public static string? GetDeviceId(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.DeviceId); + + /// <summary> + /// Get device from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Device.</returns> + public static string? GetDevice(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Device); + + /// <summary> + /// Get client from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Client.</returns> + public static string? GetClient(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Client); + + /// <summary> + /// Get version from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Version.</returns> + public static string? GetVersion(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Version); + + /// <summary> + /// Get token from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Token.</returns> + public static string? GetToken(in ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Token); + + private static string? GetClaimValue(in ClaimsPrincipal user, string name) + { + return user?.Identities + .SelectMany(c => c.Claims) + .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Select(claim => claim.Value) + .FirstOrDefault(); + } + } +} diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs index 3706a11e3..3b827ec12 100644 --- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Jellyfin.Api.Models.ConfigurationDtos { /// <summary> diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index 502b22623..af5239ec2 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using MediaBrowser.Model.Notifications; diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs index e34e176cb..64e92bd83 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs index b3746ee2d..0568dea66 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -1,5 +1,3 @@ -#nullable enable - using MediaBrowser.Model.Notifications; namespace Jellyfin.Api.Models.NotificationDtos diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs index aaaf54267..7f1255f4b 100644 --- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs +++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System; +using System; namespace Jellyfin.Api.Models.PluginDtos { diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs index 793002a6c..a90398425 100644 --- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs +++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Jellyfin.Api.Models.PluginDtos +namespace Jellyfin.Api.Models.PluginDtos { /// <summary> /// Plugin security info. diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index 5a83a030d..a5f012245 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace Jellyfin.Api.Models.StartupDtos { /// <summary> @@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos /// <summary> /// Gets or sets UI language culture. /// </summary> - public string UICulture { get; set; } + public string? UICulture { get; set; } /// <summary> /// Gets or sets the metadata country code. /// </summary> - public string MetadataCountryCode { get; set; } + public string? MetadataCountryCode { get; set; } /// <summary> /// Gets or sets the preferred language for the metadata. /// </summary> - public string PreferredMetadataLanguage { get; set; } + public string? PreferredMetadataLanguage { get; set; } } } diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs index 0dbb245ec..e4c973548 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace Jellyfin.Api.Models.StartupDtos { /// <summary> @@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos /// <summary> /// Gets or sets the username. /// </summary> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the user's password. /// </summary> - public string Password { get; set; } + public string? Password { get; set; } } } |
