< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Users.UserManager
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Users/UserManager.cs
Line coverage
58%
Covered lines: 352
Uncovered lines: 246
Coverable lines: 598
Total lines: 1114
Line coverage: 58.8%
Branch coverage
52%
Covered branches: 101
Total branches: 194
Branch coverage: 52%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 4/8/2026 - 12:11:47 AM Line coverage: 70.9% (117/165) Branch coverage: 42.5% (17/40) Total lines: 8944/19/2026 - 12:14:27 AM Line coverage: 44.4% (215/484) Branch coverage: 39% (57/146) Total lines: 8945/4/2026 - 12:15:16 AM Line coverage: 44.2% (215/486) Branch coverage: 39% (57/146) Total lines: 8965/7/2026 - 12:15:44 AM Line coverage: 46.3% (245/529) Branch coverage: 39.6% (65/164) Total lines: 9935/20/2026 - 12:15:44 AM Line coverage: 46.3% (245/529) Branch coverage: 37.1% (61/164) Total lines: 9935/27/2026 - 12:15:38 AM Line coverage: 51.8% (275/530) Branch coverage: 40.8% (67/164) Total lines: 9816/8/2026 - 12:16:15 AM Line coverage: 54.1% (323/597) Branch coverage: 46.3% (90/194) Total lines: 11137/6/2026 - 12:16:28 AM Line coverage: 58.9% (352/597) Branch coverage: 52% (101/194) Total lines: 11137/16/2026 - 12:13:45 AM Line coverage: 58.8% (352/598) Branch coverage: 52% (101/194) Total lines: 1114 4/8/2026 - 12:11:47 AM Line coverage: 70.9% (117/165) Branch coverage: 42.5% (17/40) Total lines: 8944/19/2026 - 12:14:27 AM Line coverage: 44.4% (215/484) Branch coverage: 39% (57/146) Total lines: 8945/4/2026 - 12:15:16 AM Line coverage: 44.2% (215/486) Branch coverage: 39% (57/146) Total lines: 8965/7/2026 - 12:15:44 AM Line coverage: 46.3% (245/529) Branch coverage: 39.6% (65/164) Total lines: 9935/20/2026 - 12:15:44 AM Line coverage: 46.3% (245/529) Branch coverage: 37.1% (61/164) Total lines: 9935/27/2026 - 12:15:38 AM Line coverage: 51.8% (275/530) Branch coverage: 40.8% (67/164) Total lines: 9816/8/2026 - 12:16:15 AM Line coverage: 54.1% (323/597) Branch coverage: 46.3% (90/194) Total lines: 11137/6/2026 - 12:16:28 AM Line coverage: 58.9% (352/597) Branch coverage: 52% (101/194) Total lines: 11137/16/2026 - 12:13:45 AM Line coverage: 58.8% (352/598) Branch coverage: 52% (101/194) Total lines: 1114

Coverage delta

Coverage delta 27 -27

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetUsers()100%11100%
GetUsersIds()100%210%
GetUserById(...)50%2283.33%
UserQuery(...)100%11100%
GetFirstUser()100%11100%
GetUserByName(...)50%2283.33%
RenameUser()62.5%8896.29%
UpdateUserAsync()62.5%241668.75%
CreateUserInternalAsync()100%22100%
CreateUserAsync()100%22100%
DeleteUserAsync()0%7280%
ResetPassword(...)100%210%
ChangePassword()66.66%6693.75%
GetUserDto(...)57.14%1414100%
AuthenticateUser()63.79%8005839.58%
StartForgotPasswordProcess()0%2040%
RedeemPasswordResetPin()0%2040%
InitializeAsync()66.66%6692.85%
GetAuthenticationProviders()100%210%
GetPasswordResetProviders()100%210%
UpdateConfigurationAsync()87.5%88100%
UpdatePolicyAsync()0%156120%
ClearProfileImageAsync()0%2040%
ThrowIfInvalidUsername(...)100%44100%
GetAuthenticationProvider(...)100%11100%
GetPasswordResetProvider(...)0%620%
GetAuthenticationProviders(...)30%291042.85%
GetPasswordResetProviders(...)0%2040%
AuthenticateLocalUser()75%4492.85%
AuthenticateWithProvider()50%6454.54%
UpdateUserInternalAsync()100%11100%
Dispose()100%11100%
Dispose(...)50%22100%
.ctor()100%11100%
.cctor()100%11100%
ShouldLock()100%11100%
LockAsync(...)100%22100%
AcquireLockAsync()100%11100%
Dispose()100%22100%
ThrowIfDisposed()100%11100%
Dispose()75%4480%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Users/UserManager.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.Linq;
 7using System.Text.RegularExpressions;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using AsyncKeyedLock;
 11using Jellyfin.Data;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Data.Events;
 14using Jellyfin.Data.Events.Users;
 15using Jellyfin.Database.Implementations;
 16using Jellyfin.Database.Implementations.Entities;
 17using Jellyfin.Database.Implementations.Enums;
 18using Jellyfin.Extensions;
 19using MediaBrowser.Common;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Common.Net;
 22using MediaBrowser.Controller.Authentication;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Events;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.Net;
 28using MediaBrowser.Model.Configuration;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.Users;
 31using Microsoft.EntityFrameworkCore;
 32using Microsoft.Extensions.Logging;
 33
 34namespace Jellyfin.Server.Implementations.Users
 35{
 36    /// <summary>
 37    /// Manages the creation and retrieval of <see cref="User"/> instances.
 38    /// </summary>
 39    public partial class UserManager : IUserManager, IDisposable
 40    {
 41        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 42        private readonly IEventManager _eventManager;
 43        private readonly INetworkManager _networkManager;
 44        private readonly IApplicationHost _appHost;
 45        private readonly IImageProcessor _imageProcessor;
 46        private readonly ILogger<UserManager> _logger;
 47        private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
 48        private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
 49        private readonly InvalidAuthProvider _invalidAuthProvider;
 50        private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
 51        private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
 52        private readonly IServerConfigurationManager _serverConfigurationManager;
 53
 4754        private readonly LockHelper _userLock = new();
 55
 56        /// <summary>
 57        /// Initializes a new instance of the <see cref="UserManager"/> class.
 58        /// </summary>
 59        /// <param name="dbProvider">The database provider.</param>
 60        /// <param name="eventManager">The event manager.</param>
 61        /// <param name="networkManager">The network manager.</param>
 62        /// <param name="appHost">The application host.</param>
 63        /// <param name="imageProcessor">The image processor.</param>
 64        /// <param name="logger">The logger.</param>
 65        /// <param name="serverConfigurationManager">The system config manager.</param>
 66        /// <param name="passwordResetProviders">The password reset providers.</param>
 67        /// <param name="authenticationProviders">The authentication providers.</param>
 68        public UserManager(
 69            IDbContextFactory<JellyfinDbContext> dbProvider,
 70            IEventManager eventManager,
 71            INetworkManager networkManager,
 72            IApplicationHost appHost,
 73            IImageProcessor imageProcessor,
 74            ILogger<UserManager> logger,
 75            IServerConfigurationManager serverConfigurationManager,
 76            IEnumerable<IPasswordResetProvider> passwordResetProviders,
 77            IEnumerable<IAuthenticationProvider> authenticationProviders)
 78        {
 4779            _dbProvider = dbProvider;
 4780            _eventManager = eventManager;
 4781            _networkManager = networkManager;
 4782            _appHost = appHost;
 4783            _imageProcessor = imageProcessor;
 4784            _logger = logger;
 4785            _serverConfigurationManager = serverConfigurationManager;
 86
 4787            _passwordResetProviders = passwordResetProviders.ToList();
 4788            _authenticationProviders = authenticationProviders.ToList();
 89
 4790            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 4791            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 4792            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 4793        }
 94
 95        /// <inheritdoc/>
 96        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 97
 98        /// <inheritdoc/>
 99        public IEnumerable<User> GetUsers()
 100        {
 3101            using var dbContext = _dbProvider.CreateDbContext();
 3102            return UserQuery(dbContext)
 3103                .ToArray();
 3104        }
 105
 106        /// <inheritdoc/>
 107        public IEnumerable<Guid> GetUsersIds()
 108        {
 0109            using var dbContext = _dbProvider.CreateDbContext();
 0110            return dbContext.Users
 0111                .AsNoTracking()
 0112                .Select(user => user.Id)
 0113                .ToArray();
 0114        }
 115
 116        // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
 117        // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
 118        // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes
 119        [GeneratedRegex(@"^(?!\s)[\w\ \-'._@+]+(?<!\s)$")]
 120        private static partial Regex ValidUsernameRegex();
 121
 122        /// <inheritdoc/>
 123        public User? GetUserById(Guid id)
 124        {
 416125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 416130            using var dbContext = _dbProvider.CreateDbContext();
 416131            return UserQuery(dbContext)
 416132                .FirstOrDefault(user => user.Id == id);
 416133        }
 134
 135        private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
 136        {
 547137            return dbContext.Users
 547138                            .AsSingleQuery()
 547139                            .Include(user => user.Permissions)
 547140                            .Include(user => user.Preferences)
 547141                            .Include(user => user.AccessSchedules)
 547142                            .Include(user => user.ProfileImage)
 547143                            .AsNoTracking();
 144        }
 145
 146        /// <inheritdoc/>
 147        public User? GetFirstUser()
 148        {
 19149            using var dbContext = _dbProvider.CreateDbContext();
 19150            return UserQuery(dbContext).FirstOrDefault();
 19151        }
 152
 153        /// <inheritdoc/>
 154        public User? GetUserByName(string name)
 155        {
 50156            if (string.IsNullOrWhiteSpace(name))
 157            {
 0158                throw new ArgumentException("Invalid username", nameof(name));
 159            }
 160
 50161            using var dbContext = _dbProvider.CreateDbContext();
 162#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 50163            return UserQuery(dbContext)
 50164                .FirstOrDefault(u => u.NormalizedUsername == name.ToUpperInvariant());
 165#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 50166        }
 167
 168        /// <inheritdoc/>
 169        public async Task RenameUser(Guid userId, string oldName, string newName)
 170        {
 10171            ThrowIfInvalidUsername(newName);
 172
 10173            if (oldName.Equals(newName, StringComparison.Ordinal))
 174            {
 0175                throw new ArgumentException("The new and old names must be different.");
 176            }
 177
 10178            User user = null!; // user is never actually null where its used afterwards so we can just ignore.
 10179            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 180            {
 10181                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 10182                await using (dbContext.ConfigureAwait(false))
 183                {
 184#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 10185                    if (await dbContext.Users
 10186                            .AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
 10187                            .ConfigureAwait(false))
 188                    {
 4189                        throw new ArgumentException(string.Format(
 4190                            CultureInfo.InvariantCulture,
 4191                            "A user with the name '{0}' already exists.",
 4192                            newName));
 193                    }
 194#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 195
 6196                    user = await UserQuery(dbContext)
 6197                        .AsTracking()
 6198                        .FirstOrDefaultAsync(u => u.Id == userId)
 6199                        .ConfigureAwait(false)
 6200                        ?? throw new ResourceNotFoundException(nameof(userId));
 6201                    user.Username = newName;
 6202                    user.NormalizedUsername = newName.ToUpperInvariant();
 6203                    await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 204                }
 6205            }
 206
 6207            var eventArgs = new UserUpdatedEventArgs(user);
 6208            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 6209            OnUserUpdated?.Invoke(this, eventArgs);
 6210        }
 211
 212        /// <inheritdoc/>
 213        public async Task UpdateUserAsync(User user)
 214        {
 17215            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 216            {
 17217                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 17218                await using (dbContext.ConfigureAwait(false))
 219                {
 220                    // TODO: this is a bit of a hack. Because the user entity can be created in another context, it is m
 17221                    var dbUser = await UserQuery(dbContext)
 17222                        .AsTracking()
 17223                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 17224                        .ConfigureAwait(false)
 17225                        ?? throw new ResourceNotFoundException(nameof(user.Id));
 226
 17227                    dbContext.Entry(dbUser).CurrentValues.SetValues(user);
 17228                    dbUser.Permissions.Clear();
 850229                    foreach (var permission in user.Permissions)
 230                    {
 408231                        dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value));
 232                    }
 233
 17234                    dbUser.Preferences.Clear();
 476235                    foreach (var preference in user.Preferences)
 236                    {
 221237                        dbUser.Preferences.Add(new Preference(preference.Kind, preference.Value));
 238                    }
 239
 17240                    dbUser.AccessSchedules.Clear();
 34241                    foreach (var accessSchedule in user.AccessSchedules)
 242                    {
 0243                        dbUser.AccessSchedules.Add(new AccessSchedule(accessSchedule.DayOfWeek, accessSchedule.StartHour
 244                    }
 245
 17246                    if (user.ProfileImage is null)
 247                    {
 17248                        if (dbUser.ProfileImage is not null)
 249                        {
 0250                            dbContext.Remove(dbUser.ProfileImage);
 0251                            dbUser.ProfileImage = null;
 252                        }
 253                    }
 0254                    else if (dbUser.ProfileImage is null)
 255                    {
 0256                        dbUser.ProfileImage = new Jellyfin.Database.Implementations.Entities.ImageInfo(user.ProfileImage
 0257                        {
 0258                            LastModified = user.ProfileImage.LastModified
 0259                        };
 260                    }
 261                    else
 262                    {
 0263                        dbUser.ProfileImage.Path = user.ProfileImage.Path;
 0264                        dbUser.ProfileImage.LastModified = user.ProfileImage.LastModified;
 265                    }
 266
 17267                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 268                }
 17269            }
 17270        }
 271
 272        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 273        {
 274            // TODO: Remove after user item data is migrated.
 48275            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 48276                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 48277                : 0;
 278
 48279            var user = new User(
 48280                name,
 48281                _defaultAuthenticationProvider.GetType().FullName!,
 48282                _defaultPasswordResetProvider.GetType().FullName!)
 48283            {
 48284                InternalId = max + 1
 48285            };
 286
 48287            user.AddDefaultPermissions();
 48288            user.AddDefaultPreferences();
 289
 48290            return user;
 48291        }
 292
 293        /// <inheritdoc/>
 294        public async Task<User> CreateUserAsync(string name)
 295        {
 36296            ThrowIfInvalidUsername(name);
 297
 298            User newUser;
 35299            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 35300            await using (dbContext.ConfigureAwait(false))
 301            {
 302#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 35303                if (await dbContext.Users
 35304                        .AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
 35305                        .ConfigureAwait(false))
 306                {
 4307                    throw new ArgumentException(string.Format(
 4308                        CultureInfo.InvariantCulture,
 4309                        "A user with the name '{0}' already exists.",
 4310                        name));
 311                }
 312#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 313
 31314                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 315
 31316                dbContext.Users.Add(newUser);
 31317                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 318            }
 319
 31320            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 321
 31322            return newUser;
 31323        }
 324
 325        /// <inheritdoc/>
 326        public async Task DeleteUserAsync(Guid userId)
 327        {
 328            User? user;
 0329            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 330            {
 0331                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0332                await using (dbContext.ConfigureAwait(false))
 333                {
 0334                    user = await dbContext.Users
 0335                        .Include(u => u.Permissions)
 0336                        .FirstOrDefaultAsync(u => u.Id.Equals(userId))
 0337                        .ConfigureAwait(false);
 0338                    if (user is null)
 339                    {
 0340                        throw new ResourceNotFoundException(nameof(userId));
 341                    }
 342
 0343                    var userCount = await dbContext.Users.CountAsync().ConfigureAwait(false);
 0344                    if (userCount == 1)
 345                    {
 0346                        throw new InvalidOperationException(string.Format(
 0347                            CultureInfo.InvariantCulture,
 0348                            "The user '{0}' cannot be deleted because there must be at least one user in the system.",
 0349                            user.Username));
 350                    }
 351
 0352                    if (user.HasPermission(PermissionKind.IsAdministrator)
 0353                        && await dbContext.Users
 0354                            .CountAsync(i => i.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value)
 0355                            .ConfigureAwait(false) == 1)
 356                    {
 0357                        throw new ArgumentException(
 0358                            string.Format(
 0359                                CultureInfo.InvariantCulture,
 0360                                "The user '{0}' cannot be deleted because there must be at least one admin user in the s
 0361                                user.Username),
 0362                            nameof(userId));
 363                    }
 364
 0365                    dbContext.Users.Remove(user);
 0366                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 367                }
 0368            }
 369
 0370            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 0371        }
 372
 373        /// <inheritdoc/>
 374        public Task ResetPassword(Guid userId)
 375        {
 0376            return ChangePassword(userId, string.Empty);
 377        }
 378
 379        /// <inheritdoc/>
 380        public async Task ChangePassword(Guid userId, string newPassword)
 381        {
 3382            User dbUser = null!;
 3383            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 384            {
 3385                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 3386                await using (dbContext.ConfigureAwait(false))
 387                {
 3388                    dbUser = await UserQuery(dbContext)
 3389                        .AsTracking()
 3390                        .FirstOrDefaultAsync(u => u.Id == userId)
 3391                        .ConfigureAwait(false)
 3392                        ?? throw new ResourceNotFoundException(nameof(userId));
 3393                    if (dbUser.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
 394                    {
 0395                        throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
 396                    }
 397
 3398                    await GetAuthenticationProvider(dbUser).ChangePassword(dbUser, newPassword).ConfigureAwait(false);
 3399                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 400                }
 3401            }
 402
 3403            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(dbUser)).ConfigureAwait(false);
 3404        }
 405
 406        /// <inheritdoc/>
 407        public UserDto GetUserDto(User user, string? remoteEndPoint = null)
 408        {
 39409            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 39410            return new UserDto
 39411            {
 39412                Name = user.Username,
 39413                Id = user.Id,
 39414                ServerId = _appHost.SystemId,
 39415                EnableAutoLogin = user.EnableAutoLogin,
 39416                LastLoginDate = user.LastLoginDate,
 39417                LastActivityDate = user.LastActivityDate,
 39418                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 39419                Configuration = new UserConfiguration
 39420                {
 39421                    SubtitleMode = user.SubtitleMode,
 39422                    HidePlayedInLatest = user.HidePlayedInLatest,
 39423                    EnableLocalPassword = user.EnableLocalPassword,
 39424                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 39425                    DisplayCollectionsView = user.DisplayCollectionsView,
 39426                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 39427                    AudioLanguagePreference = user.AudioLanguagePreference,
 39428                    RememberAudioSelections = user.RememberAudioSelections,
 39429                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 39430                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 39431                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 39432                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 39433                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 39434                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 39435                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 39436                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 39437                        ? castReceiverApplications.FirstOrDefault()?.Id
 39438                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 39439                          ?? castReceiverApplications.FirstOrDefault()?.Id
 39440                },
 39441                Policy = new UserPolicy
 39442                {
 39443                    MaxParentalRating = user.MaxParentalRatingScore,
 39444                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
 39445                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 39446                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 39447                    AuthenticationProviderId = user.AuthenticationProviderId,
 39448                    PasswordResetProviderId = user.PasswordResetProviderId,
 39449                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 39450                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 39451                    MaxActiveSessions = user.MaxActiveSessions,
 39452                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 39453                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 39454                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 39455                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 39456                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 39457                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 39458                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 39459                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 39460                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 39461                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 39462                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 39463                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 39464                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 39465                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 39466                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 39467                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 39468                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 39469                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 39470                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 39471                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 39472                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 39473                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 39474                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 39475                    AccessSchedules = user.AccessSchedules.ToArray(),
 39476                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 39477                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 39478                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 39479                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 39480                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 39481                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 39482                    SyncPlayAccess = user.SyncPlayAccess,
 39483                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 39484                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 39485                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 39486                }
 39487            };
 488        }
 489
 490        /// <inheritdoc/>
 491        public async Task<User?> AuthenticateUser(
 492            string username,
 493            string password,
 494            string remoteEndPoint,
 495            bool isUserSession)
 496        {
 16497            if (string.IsNullOrWhiteSpace(username))
 498            {
 0499                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndP
 0500                throw new ArgumentNullException(nameof(username));
 501            }
 502
 503            bool success;
 16504            var user = GetUserByName(username);
 16505            using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false))
 506            {
 16507                using var dbContext = _dbProvider.CreateDbContext();
 508
 509                // Reload the user now that we hold the lock so the RowVersion is current.
 510                // GetUserByName uses AsNoTracking and the snapshot may be stale if another
 511                // write (e.g. a concurrent login) incremented RowVersion after our initial load.
 16512                if (user is not null)
 513                {
 16514                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false) ??
 515                }
 516
 16517                var authResult = await AuthenticateLocalUser(username, password, user)
 16518                    .ConfigureAwait(false);
 16519                var authenticationProvider = authResult.AuthenticationProvider;
 16520                success = authResult.Success;
 521
 16522                if (success && user is not null)
 523                {
 524                    // refresh the user if the auth provider might have updated it in the auth method.
 525                    // this is a hack, this needs removal once the LDAP plugin uses the correct interface to get the use
 16526                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false);
 527                }
 528
 16529                if (user is null)
 530                {
 0531                    string updatedUsername = authResult.Username;
 532
 0533                    if (success
 0534                        && authenticationProvider is not null
 0535                        && authenticationProvider is not DefaultAuthenticationProvider)
 536                    {
 537                        // Trust the username returned by the authentication provider
 0538                        username = updatedUsername;
 539
 540                        // Search the database for the user again
 541                        // the authentication provider might have created it
 542#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 0543                        user = await UserQuery(dbContext)
 0544                            .FirstOrDefaultAsync(e => e.NormalizedUsername == username.ToUpperInvariant()).ConfigureAwai
 545
 0546                        if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
 547                        {
 0548                            await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
 0549                            user = await UserQuery(dbContext)
 0550                                .FirstOrDefaultAsync(e => e.NormalizedUsername == username.ToUpperInvariant()).Configure
 551#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 552                        }
 553                    }
 554                }
 555
 16556                if (success && user is not null && authenticationProvider is not null)
 557                {
 16558                    var providerId = authenticationProvider.GetType().FullName;
 559
 16560                    if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringCompar
 561                    {
 0562                        await dbContext.Users
 0563                            .Where(e => e.Id == user.Id)
 0564                            .ExecuteUpdateAsync(e => e.SetProperty(f => f.AuthenticationProviderId, providerId))
 0565                            .ConfigureAwait(false);
 566                    }
 567                }
 568
 16569                if (user is null)
 570                {
 0571                    _logger.LogInformation(
 0572                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0573                        username,
 0574                        remoteEndPoint);
 0575                    throw new AuthenticationException("Invalid username or password entered.");
 576                }
 577
 16578                if (user.HasPermission(PermissionKind.IsDisabled))
 579                {
 0580                    _logger.LogInformation(
 0581                        "Authentication request for {UserName} has been denied because this account is currently disable
 0582                        username,
 0583                        remoteEndPoint);
 0584                    throw new SecurityException(
 0585                        $"The {user.Username} account is currently disabled. Please consult with your administrator.");
 586                }
 587
 16588                if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 16589                    !_networkManager.IsInLocalNetwork(remoteEndPoint))
 590                {
 0591                    _logger.LogInformation(
 0592                        "Authentication request for {UserName} forbidden: remote access disabled and user not in local n
 0593                        username,
 0594                        remoteEndPoint);
 0595                    throw new SecurityException("Forbidden.");
 596                }
 597
 16598                if (!user.IsParentalScheduleAllowed())
 599                {
 0600                    _logger.LogInformation(
 0601                        "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP
 0602                        username,
 0603                        remoteEndPoint);
 0604                    throw new SecurityException("User is not allowed access at this time.");
 605                }
 606
 607                // Update LastActivityDate and LastLoginDate, then save
 16608                if (success)
 609                {
 16610                    if (isUserSession)
 611                    {
 16612                        var date = DateTime.UtcNow;
 16613                        await dbContext.Users
 16614                            .Where(e => e.Id == user.Id)
 16615                            .ExecuteUpdateAsync(e => e
 16616                                .SetProperty(f => f.LastActivityDate, date)
 16617                                .SetProperty(f => f.LastLoginDate, date))
 16618                            .ConfigureAwait(false);
 619                    }
 620
 16621                    await dbContext.Users
 16622                        .Where(e => e.Id == user.Id)
 16623                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, 0))
 16624                        .ConfigureAwait(false);
 16625                    _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 626                }
 627                else
 628                {
 0629                    user.InvalidLoginAttemptCount++;
 0630                    int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 0631                    if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 632                    {
 0633                        user.SetPermission(PermissionKind.IsDisabled, true);
 0634                        dbContext.Update(user);
 0635                        await dbContext.SaveChangesAsync()
 0636                            .ConfigureAwait(false);
 0637                        await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 0638                        _logger.LogWarning(
 0639                            "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 0640                            user.Username,
 0641                            user.InvalidLoginAttemptCount);
 642                    }
 643
 0644                    await dbContext.Users
 0645                        .Where(e => e.Id == user.Id)
 0646                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, f => f.InvalidLoginAttem
 0647                        .ConfigureAwait(false);
 648
 0649                    _logger.LogInformation(
 0650                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0651                        user.Username,
 0652                        remoteEndPoint);
 653                }
 16654            }
 655
 16656            return success ? user : null;
 16657        }
 658
 659        /// <inheritdoc/>
 660        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 661        {
 0662            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 0663            var passwordResetProvider = GetPasswordResetProvider(user);
 664
 0665            var result = await passwordResetProvider
 0666                .StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
 0667                .ConfigureAwait(false);
 668
 0669            if (user is not null && isInNetwork)
 670            {
 0671                await UpdateUserAsync(user).ConfigureAwait(false);
 672            }
 673
 0674            return result;
 0675        }
 676
 677        /// <inheritdoc/>
 678        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 679        {
 0680            foreach (var provider in _passwordResetProviders)
 681            {
 0682                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 683
 0684                if (result.Success)
 685                {
 0686                    return result;
 687                }
 688            }
 689
 0690            return new PinRedeemResult();
 0691        }
 692
 693        /// <inheritdoc />
 694        public async Task InitializeAsync()
 695        {
 696            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 18697            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 18698            await using (dbContext.ConfigureAwait(false))
 699            {
 18700                if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
 701                {
 702                    return;
 703                }
 704
 17705                var defaultName = Environment.UserName;
 17706                if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 707                {
 0708                    defaultName = "MyJellyfinUser";
 709                }
 710
 17711                _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 712
 17713                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 17714                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 17715                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 17716                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 717
 17718                dbContext.Users.Add(newUser);
 17719                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 720            }
 18721        }
 722
 723        /// <inheritdoc/>
 724        public NameIdPair[] GetAuthenticationProviders()
 725        {
 0726            return _authenticationProviders
 0727                .Where(provider => provider.IsEnabled)
 0728                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0729                .ThenBy(i => i.Name)
 0730                .Select(i => new NameIdPair
 0731                {
 0732                    Name = i.Name,
 0733                    Id = i.GetType().FullName
 0734                })
 0735                .ToArray();
 736        }
 737
 738        /// <inheritdoc/>
 739        public NameIdPair[] GetPasswordResetProviders()
 740        {
 0741            return _passwordResetProviders
 0742                .Where(provider => provider.IsEnabled)
 0743                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0744                .ThenBy(i => i.Name)
 0745                .Select(i => new NameIdPair
 0746                {
 0747                    Name = i.Name,
 0748                    Id = i.GetType().FullName
 0749                })
 0750                .ToArray();
 751        }
 752
 753        /// <inheritdoc/>
 754        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 755        {
 1756            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 757            {
 1758                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1759                await using (dbContext.ConfigureAwait(false))
 760                {
 1761                    var user = UserQuery(dbContext)
 1762                                   .AsTracking()
 1763                                   .FirstOrDefault(u => u.Id.Equals(userId))
 1764                               ?? throw new ArgumentException("No user exists with given Id!");
 765
 1766                    user.SubtitleMode = config.SubtitleMode;
 1767                    user.HidePlayedInLatest = config.HidePlayedInLatest;
 1768                    user.EnableLocalPassword = config.EnableLocalPassword;
 1769                    user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 1770                    user.DisplayCollectionsView = config.DisplayCollectionsView;
 1771                    user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 1772                    user.AudioLanguagePreference = config.AudioLanguagePreference;
 1773                    user.RememberAudioSelections = config.RememberAudioSelections;
 1774                    user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 1775                    user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 1776                    user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 777
 778                    // Only set cast receiver id if it is passed in and it exists in the server config.
 1779                    if (!string.IsNullOrEmpty(config.CastReceiverId)
 1780                        && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.I
 781                    {
 1782                        user.CastReceiverId = config.CastReceiverId;
 783                    }
 784
 1785                    user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 1786                    user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 1787                    user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 1788                    user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 789
 1790                    dbContext.Update(user);
 1791                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 792                }
 1793            }
 1794        }
 795
 796        /// <inheritdoc/>
 797        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 798        {
 0799            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 800            {
 0801                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0802                await using (dbContext.ConfigureAwait(false))
 803                {
 0804                    var user = UserQuery(dbContext)
 0805                        .AsTracking()
 0806                        .FirstOrDefault(u => u.Id.Equals(userId))
 0807                        ?? throw new ArgumentException("No user exists with given Id!");
 808
 809                    // The default number of login attempts is 3, but for some god forsaken reason it's sent to the serv
 0810                    int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 0811                    {
 0812                        -1 => null,
 0813                        0 => 3,
 0814                        _ => policy.LoginAttemptsBeforeLockout
 0815                    };
 816
 0817                    user.MaxParentalRatingScore = policy.MaxParentalRating;
 0818                    user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 0819                    user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 0820                    user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 0821                    user.AuthenticationProviderId = policy.AuthenticationProviderId;
 0822                    user.PasswordResetProviderId = policy.PasswordResetProviderId;
 0823                    user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 0824                    user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 0825                    user.MaxActiveSessions = policy.MaxActiveSessions;
 0826                    user.SyncPlayAccess = policy.SyncPlayAccess;
 0827                    user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 0828                    user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 0829                    user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 0830                    user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 0831                    user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 0832                    user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 0833                    user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 0834                    user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 0835                    user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscod
 0836                    user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscod
 0837                    user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 0838                    user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 0839                    user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 0840                    user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 0841                    user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 0842                    user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 0843                    user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 0844                    user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOther
 0845                    user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 0846                    user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 0847                    user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 0848                    user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 0849                    user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding)
 0850                    user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 851
 0852                    user.AccessSchedules.Clear();
 0853                    foreach (var policyAccessSchedule in policy.AccessSchedules)
 854                    {
 0855                        user.AccessSchedules.Add(policyAccessSchedule);
 856                    }
 857
 858                    // TODO: fix this at some point
 0859                    user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<Unrated
 0860                    user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 0861                    user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 0862                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 0863                    user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 0864                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 0865                    user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFrom
 866
 0867                    dbContext.Update(user);
 0868                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 869                }
 0870            }
 0871        }
 872
 873        /// <inheritdoc/>
 874        public async Task ClearProfileImageAsync(User user)
 875        {
 0876            if (user.ProfileImage is null)
 877            {
 0878                return;
 879            }
 880
 0881            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 882            {
 0883                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0884                await using (dbContext.ConfigureAwait(false))
 885                {
 0886                    dbContext.Remove(user.ProfileImage);
 0887                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 888                }
 889
 0890                user.ProfileImage = null;
 0891            }
 0892        }
 893
 894        internal static void ThrowIfInvalidUsername(string name)
 895        {
 59896            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
 897            {
 51898                return;
 899            }
 900
 8901            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 902        }
 903
 904        private IAuthenticationProvider GetAuthenticationProvider(User user)
 905        {
 3906            return GetAuthenticationProviders(user)[0];
 907        }
 908
 909        private IPasswordResetProvider GetPasswordResetProvider(User? user)
 910        {
 0911            if (user is null)
 912            {
 0913                return _defaultPasswordResetProvider;
 914            }
 915
 0916            return GetPasswordResetProviders(user)[0];
 917        }
 918
 919        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 920        {
 19921            var authenticationProviderId = user?.AuthenticationProviderId;
 922
 19923            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 924
 19925            if (!string.IsNullOrEmpty(authenticationProviderId))
 926            {
 19927                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 928            }
 929
 19930            if (providers.Count == 0)
 931            {
 932                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0933                _logger.LogWarning(
 0934                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0935                    user?.Username,
 0936                    user?.AuthenticationProviderId);
 0937                providers = new List<IAuthenticationProvider>
 0938                {
 0939                    _invalidAuthProvider
 0940                };
 941            }
 942
 19943            return providers;
 944        }
 945
 946        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 947        {
 0948            var passwordResetProviderId = user.PasswordResetProviderId;
 0949            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 950
 0951            if (!string.IsNullOrEmpty(passwordResetProviderId))
 952            {
 0953                providers = providers.Where(i =>
 0954                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0955                    .ToArray();
 956            }
 957
 0958            if (providers.Length == 0)
 959            {
 0960                providers = new IPasswordResetProvider[]
 0961                {
 0962                    _defaultPasswordResetProvider
 0963                };
 964            }
 965
 0966            return providers;
 967        }
 968
 969        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 970                string username,
 971                string password,
 972                User? user)
 973        {
 16974            bool success = false;
 16975            IAuthenticationProvider? authenticationProvider = null;
 976
 48977            foreach (var provider in GetAuthenticationProviders(user))
 978            {
 16979                var providerAuthResult =
 16980                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 16981                var updatedUsername = providerAuthResult.Username;
 16982                success = providerAuthResult.Success;
 983
 16984                if (success)
 985                {
 16986                    authenticationProvider = provider;
 16987                    username = updatedUsername;
 16988                    break;
 989                }
 0990            }
 991
 16992            return (authenticationProvider, username, success);
 16993        }
 994
 995        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 996            IAuthenticationProvider provider,
 997            string username,
 998            string password,
 999            User? resolvedUser)
 1000        {
 1001            try
 1002            {
 161003                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 161004                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 161005                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 1006
 161007                if (authenticationResult.Username != username)
 1008                {
 01009                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 01010                    username = authenticationResult.Username;
 1011                }
 1012
 161013                return (username, true);
 1014            }
 01015            catch (AuthenticationException ex)
 1016            {
 01017                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 1018
 01019                return (username, false);
 1020            }
 161021        }
 1022
 1023        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 1024        {
 61025            dbContext.Users.Attach(user);
 61026            dbContext.Entry(user).State = EntityState.Modified;
 61027            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 61028        }
 1029
 1030        /// <inheritdoc/>
 1031        public void Dispose()
 1032        {
 471033            Dispose(true);
 471034            GC.SuppressFinalize(this);
 471035        }
 1036
 1037        /// <summary>
 1038        /// Disposes all members of this class.
 1039        /// </summary>
 1040        /// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
 1041        protected virtual void Dispose(bool disposing)
 1042        {
 471043            if (disposing)
 1044            {
 471045                _userLock.Dispose();
 1046            }
 471047        }
 1048
 1049        internal sealed class LockHelper : IDisposable
 1050        {
 511051            private readonly AsyncKeyedLocker<Guid> _userLock = new();
 1052
 1053            private bool _disposed;
 1054
 21055            public static AsyncLocal<int> IsNestedLock { get; set; } = new();
 1056
 1057            public bool ShouldLock()
 1058            {
 51059                return IsNestedLock.Value == 0;
 1060            }
 1061
 1062            public ValueTask<IDisposable> LockAsync(Guid key)
 1063            {
 521064                ThrowIfDisposed();
 511065                var isNested = LockHelper.IsNestedLock.Value != 0;
 511066                LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value + 1;
 511067                if (isNested)
 1068                {
 11069                    return new ValueTask<IDisposable>(new LockHandle { Parent = null });
 1070                }
 1071
 501072                return AcquireLockAsync(key);
 1073            }
 1074
 1075            private async ValueTask<IDisposable> AcquireLockAsync(Guid key)
 1076            {
 501077                var lockHandle = await _userLock.LockAsync(key, true).ConfigureAwait(false);
 501078                return new LockHandle { Parent = lockHandle };
 501079            }
 1080
 1081            public void Dispose()
 1082            {
 541083                if (_disposed)
 1084                {
 31085                    return;
 1086                }
 1087
 511088                _disposed = true;
 511089                _userLock.Dispose();
 511090            }
 1091
 1092            private void ThrowIfDisposed()
 1093            {
 521094                ObjectDisposedException.ThrowIf(_disposed, this);
 511095            }
 1096
 1097            private sealed class LockHandle : IDisposable
 1098            {
 1099                public required IDisposable? Parent { get; init; }
 1100
 1101                public void Dispose()
 1102                {
 511103                    Parent?.Dispose();
 511104                    LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value - 1;
 1105
 511106                    if (LockHelper.IsNestedLock.Value < 0)
 1107                    {
 01108                        throw new InvalidOperationException("Mismatched locking detected. Threads internal NestedLock is
 1109                    }
 511110                }
 1111            }
 1112        }
 1113    }
 1114}

Methods/Properties

.ctor(Microsoft.EntityFrameworkCore.IDbContextFactory`1<Jellyfin.Database.Implementations.JellyfinDbContext>,MediaBrowser.Controller.Events.IEventManager,MediaBrowser.Common.Net.INetworkManager,MediaBrowser.Common.IApplicationHost,MediaBrowser.Controller.Drawing.IImageProcessor,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Server.Implementations.Users.UserManager>,MediaBrowser.Controller.Configuration.IServerConfigurationManager,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Authentication.IPasswordResetProvider>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Authentication.IAuthenticationProvider>)
GetUsers()
GetUsersIds()
GetUserById(System.Guid)
UserQuery(Jellyfin.Database.Implementations.JellyfinDbContext)
GetFirstUser()
GetUserByName(System.String)
RenameUser()
UpdateUserAsync()
CreateUserInternalAsync()
CreateUserAsync()
DeleteUserAsync()
ResetPassword(System.Guid)
ChangePassword()
GetUserDto(Jellyfin.Database.Implementations.Entities.User,System.String)
AuthenticateUser()
StartForgotPasswordProcess()
RedeemPasswordResetPin()
InitializeAsync()
GetAuthenticationProviders()
GetPasswordResetProviders()
UpdateConfigurationAsync()
UpdatePolicyAsync()
ClearProfileImageAsync()
ThrowIfInvalidUsername(System.String)
GetAuthenticationProvider(Jellyfin.Database.Implementations.Entities.User)
GetPasswordResetProvider(Jellyfin.Database.Implementations.Entities.User)
GetAuthenticationProviders(Jellyfin.Database.Implementations.Entities.User)
GetPasswordResetProviders(Jellyfin.Database.Implementations.Entities.User)
AuthenticateLocalUser()
AuthenticateWithProvider()
UpdateUserInternalAsync()
Dispose()
Dispose(System.Boolean)
.ctor()
.cctor()
ShouldLock()
LockAsync(System.Guid)
AcquireLockAsync()
Dispose()
ThrowIfDisposed()
Dispose()