< 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
54%
Covered lines: 323
Uncovered lines: 274
Coverable lines: 597
Total lines: 1113
Line coverage: 54.1%
Branch coverage
46%
Covered branches: 90
Total branches: 194
Branch coverage: 46.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/5/2026 - 12:13:57 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: 1113 3/5/2026 - 12:13:57 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: 1113

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(...)28.57%1414100%
AuthenticateUser()63.79%7855840%
StartForgotPasswordProcess()0%2040%
RedeemPasswordResetPin()0%2040%
InitializeAsync()66.66%6692.85%
GetAuthenticationProviders()100%210%
GetPasswordResetProviders()100%210%
UpdateConfigurationAsync()0%7280%
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
 4654        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        {
 4679            _dbProvider = dbProvider;
 4680            _eventManager = eventManager;
 4681            _networkManager = networkManager;
 4682            _appHost = appHost;
 4683            _imageProcessor = imageProcessor;
 4684            _logger = logger;
 4685            _serverConfigurationManager = serverConfigurationManager;
 86
 4687            _passwordResetProviders = passwordResetProviders.ToList();
 4688            _authenticationProviders = authenticationProviders.ToList();
 89
 4690            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 4691            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 4692            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 4693        }
 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        {
 244125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 244130            using var dbContext = _dbProvider.CreateDbContext();
 244131            return UserQuery(dbContext)
 244132                .FirstOrDefault(user => user.Id == id);
 244133        }
 134
 135        private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
 136        {
 367137            return dbContext.Users
 367138                            .AsSingleQuery()
 367139                            .Include(user => user.Permissions)
 367140                            .Include(user => user.Preferences)
 367141                            .Include(user => user.AccessSchedules)
 367142                            .Include(user => user.ProfileImage)
 367143                            .AsNoTracking();
 144        }
 145
 146        /// <inheritdoc/>
 147        public User? GetFirstUser()
 148        {
 18149            using var dbContext = _dbProvider.CreateDbContext();
 18150            return UserQuery(dbContext).FirstOrDefault();
 18151        }
 152
 153        /// <inheritdoc/>
 154        public User? GetUserByName(string name)
 155        {
 48156            if (string.IsNullOrWhiteSpace(name))
 157            {
 0158                throw new ArgumentException("Invalid username", nameof(name));
 159            }
 160
 48161            using var dbContext = _dbProvider.CreateDbContext();
 162#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 48163            return UserQuery(dbContext)
 48164                .FirstOrDefault(u => u.NormalizedUsername == name.ToUpperInvariant());
 165#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 48166        }
 167
 168        /// <inheritdoc/>
 169        public async Task RenameUser(Guid userId, string oldName, string newName)
 170        {
 9171            ThrowIfInvalidUsername(newName);
 172
 9173            if (oldName.Equals(newName, StringComparison.OrdinalIgnoreCase))
 174            {
 0175                throw new ArgumentException("The new and old names must be different.");
 176            }
 177
 9178            User user = null!; // user is never actually null where its used afterwards so we can just ignore.
 9179            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 180            {
 9181                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 9182                await using (dbContext.ConfigureAwait(false))
 183                {
 184#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 9185                    if (await dbContext.Users
 9186                            .AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
 9187                            .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
 5196                    user = await UserQuery(dbContext)
 5197                        .AsTracking()
 5198                        .FirstOrDefaultAsync(u => u.Id == userId)
 5199                        .ConfigureAwait(false)
 5200                        ?? throw new ResourceNotFoundException(nameof(userId));
 5201                    user.Username = newName;
 5202                    user.NormalizedUsername = newName.ToUpperInvariant();
 5203                    await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 204                }
 5205            }
 206
 5207            var eventArgs = new UserUpdatedEventArgs(user);
 5208            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 5209            OnUserUpdated?.Invoke(this, eventArgs);
 5210        }
 211
 212        /// <inheritdoc/>
 213        public async Task UpdateUserAsync(User user)
 214        {
 16215            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 216            {
 16217                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 16218                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
 16221                    var dbUser = await UserQuery(dbContext)
 16222                        .AsTracking()
 16223                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 16224                        .ConfigureAwait(false)
 16225                        ?? throw new ResourceNotFoundException(nameof(user.Id));
 226
 16227                    dbContext.Entry(dbUser).CurrentValues.SetValues(user);
 16228                    dbUser.Permissions.Clear();
 800229                    foreach (var permission in user.Permissions)
 230                    {
 384231                        dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value));
 232                    }
 233
 16234                    dbUser.Preferences.Clear();
 448235                    foreach (var preference in user.Preferences)
 236                    {
 208237                        dbUser.Preferences.Add(new Preference(preference.Kind, preference.Value));
 238                    }
 239
 16240                    dbUser.AccessSchedules.Clear();
 32241                    foreach (var accessSchedule in user.AccessSchedules)
 242                    {
 0243                        dbUser.AccessSchedules.Add(new AccessSchedule(accessSchedule.DayOfWeek, accessSchedule.StartHour
 244                    }
 245
 16246                    if (user.ProfileImage is null)
 247                    {
 16248                        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
 16267                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 268                }
 16269            }
 16270        }
 271
 272        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 273        {
 274            // TODO: Remove after user item data is migrated.
 47275            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 47276                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 47277                : 0;
 278
 47279            var user = new User(
 47280                name,
 47281                _defaultAuthenticationProvider.GetType().FullName!,
 47282                _defaultPasswordResetProvider.GetType().FullName!)
 47283            {
 47284                InternalId = max + 1
 47285            };
 286
 47287            user.AddDefaultPermissions();
 47288            user.AddDefaultPreferences();
 289
 47290            return user;
 47291        }
 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        {
 35409            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 35410            return new UserDto
 35411            {
 35412                Name = user.Username,
 35413                Id = user.Id,
 35414                ServerId = _appHost.SystemId,
 35415                EnableAutoLogin = user.EnableAutoLogin,
 35416                LastLoginDate = user.LastLoginDate,
 35417                LastActivityDate = user.LastActivityDate,
 35418                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 35419                Configuration = new UserConfiguration
 35420                {
 35421                    SubtitleMode = user.SubtitleMode,
 35422                    HidePlayedInLatest = user.HidePlayedInLatest,
 35423                    EnableLocalPassword = user.EnableLocalPassword,
 35424                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 35425                    DisplayCollectionsView = user.DisplayCollectionsView,
 35426                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 35427                    AudioLanguagePreference = user.AudioLanguagePreference,
 35428                    RememberAudioSelections = user.RememberAudioSelections,
 35429                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 35430                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 35431                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 35432                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 35433                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 35434                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 35435                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 35436                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 35437                        ? castReceiverApplications.FirstOrDefault()?.Id
 35438                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 35439                          ?? castReceiverApplications.FirstOrDefault()?.Id
 35440                },
 35441                Policy = new UserPolicy
 35442                {
 35443                    MaxParentalRating = user.MaxParentalRatingScore,
 35444                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
 35445                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 35446                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 35447                    AuthenticationProviderId = user.AuthenticationProviderId,
 35448                    PasswordResetProviderId = user.PasswordResetProviderId,
 35449                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 35450                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 35451                    MaxActiveSessions = user.MaxActiveSessions,
 35452                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 35453                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 35454                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 35455                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 35456                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 35457                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 35458                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 35459                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 35460                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 35461                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 35462                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 35463                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 35464                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 35465                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 35466                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 35467                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 35468                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 35469                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 35470                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 35471                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 35472                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 35473                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 35474                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 35475                    AccessSchedules = user.AccessSchedules.ToArray(),
 35476                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 35477                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 35478                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 35479                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 35480                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 35481                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 35482                    SyncPlayAccess = user.SyncPlayAccess,
 35483                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 35484                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 35485                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 35486                }
 35487            };
 488        }
 489
 490        /// <inheritdoc/>
 491        public async Task<User?> AuthenticateUser(
 492            string username,
 493            string password,
 494            string remoteEndPoint,
 495            bool isUserSession)
 496        {
 15497            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;
 15504            var user = GetUserByName(username);
 15505            using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false))
 506            {
 15507                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.
 15512                if (user is not null)
 513                {
 15514                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false) ??
 515                }
 516
 15517                var authResult = await AuthenticateLocalUser(username, password, user)
 15518                    .ConfigureAwait(false);
 15519                var authenticationProvider = authResult.AuthenticationProvider;
 15520                success = authResult.Success;
 521
 15522                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
 15526                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false);
 527                }
 528
 15529                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
 15556                if (success && user is not null && authenticationProvider is not null)
 557                {
 15558                    var providerId = authenticationProvider.GetType().FullName;
 559
 15560                    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
 15569                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
 15578                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
 15588                if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 15589                    !_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
 15598                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
 15608                if (success)
 609                {
 15610                    if (isUserSession)
 611                    {
 15612                        var date = DateTime.UtcNow;
 15613                        await dbContext.Users
 15614                            .Where(e => e.Id == user.Id)
 15615                            .ExecuteUpdateAsync(e => e
 15616                                .SetProperty(f => f.LastActivityDate, date)
 15617                                .SetProperty(f => f.LastLoginDate, date))
 15618                            .ConfigureAwait(false);
 619                    }
 620
 15621                    await dbContext.Users
 15622                        .Where(e => e.Id == user.Id)
 15623                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, 0))
 15624                        .ConfigureAwait(false);
 15625                    _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                        await dbContext.SaveChangesAsync()
 0635                            .ConfigureAwait(false);
 0636                        await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 0637                        _logger.LogWarning(
 0638                            "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 0639                            user.Username,
 0640                            user.InvalidLoginAttemptCount);
 641                    }
 642
 0643                    await dbContext.Users
 0644                        .Where(e => e.Id == user.Id)
 0645                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, f => f.InvalidLoginAttem
 0646                        .ConfigureAwait(false);
 647
 0648                    _logger.LogInformation(
 0649                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0650                        user.Username,
 0651                        remoteEndPoint);
 652                }
 15653            }
 654
 15655            return success ? user : null;
 15656        }
 657
 658        /// <inheritdoc/>
 659        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 660        {
 0661            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 0662            var passwordResetProvider = GetPasswordResetProvider(user);
 663
 0664            var result = await passwordResetProvider
 0665                .StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
 0666                .ConfigureAwait(false);
 667
 0668            if (user is not null && isInNetwork)
 669            {
 0670                await UpdateUserAsync(user).ConfigureAwait(false);
 671            }
 672
 0673            return result;
 0674        }
 675
 676        /// <inheritdoc/>
 677        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 678        {
 0679            foreach (var provider in _passwordResetProviders)
 680            {
 0681                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 682
 0683                if (result.Success)
 684                {
 0685                    return result;
 686                }
 687            }
 688
 0689            return new PinRedeemResult();
 0690        }
 691
 692        /// <inheritdoc />
 693        public async Task InitializeAsync()
 694        {
 695            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 17696            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 17697            await using (dbContext.ConfigureAwait(false))
 698            {
 17699                if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
 700                {
 701                    return;
 702                }
 703
 16704                var defaultName = Environment.UserName;
 16705                if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 706                {
 0707                    defaultName = "MyJellyfinUser";
 708                }
 709
 16710                _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 711
 16712                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 16713                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 16714                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 16715                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 716
 16717                dbContext.Users.Add(newUser);
 16718                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 719            }
 17720        }
 721
 722        /// <inheritdoc/>
 723        public NameIdPair[] GetAuthenticationProviders()
 724        {
 0725            return _authenticationProviders
 0726                .Where(provider => provider.IsEnabled)
 0727                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0728                .ThenBy(i => i.Name)
 0729                .Select(i => new NameIdPair
 0730                {
 0731                    Name = i.Name,
 0732                    Id = i.GetType().FullName
 0733                })
 0734                .ToArray();
 735        }
 736
 737        /// <inheritdoc/>
 738        public NameIdPair[] GetPasswordResetProviders()
 739        {
 0740            return _passwordResetProviders
 0741                .Where(provider => provider.IsEnabled)
 0742                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0743                .ThenBy(i => i.Name)
 0744                .Select(i => new NameIdPair
 0745                {
 0746                    Name = i.Name,
 0747                    Id = i.GetType().FullName
 0748                })
 0749                .ToArray();
 750        }
 751
 752        /// <inheritdoc/>
 753        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 754        {
 0755            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 756            {
 0757                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0758                await using (dbContext.ConfigureAwait(false))
 759                {
 0760                    var user = UserQuery(dbContext)
 0761                                   .AsTracking()
 0762                                   .FirstOrDefault(u => u.Id.Equals(userId))
 0763                               ?? throw new ArgumentException("No user exists with given Id!");
 764
 0765                    user.SubtitleMode = config.SubtitleMode;
 0766                    user.HidePlayedInLatest = config.HidePlayedInLatest;
 0767                    user.EnableLocalPassword = config.EnableLocalPassword;
 0768                    user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 0769                    user.DisplayCollectionsView = config.DisplayCollectionsView;
 0770                    user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 0771                    user.AudioLanguagePreference = config.AudioLanguagePreference;
 0772                    user.RememberAudioSelections = config.RememberAudioSelections;
 0773                    user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 0774                    user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 0775                    user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 776
 777                    // Only set cast receiver id if it is passed in and it exists in the server config.
 0778                    if (!string.IsNullOrEmpty(config.CastReceiverId)
 0779                        && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.I
 780                    {
 0781                        user.CastReceiverId = config.CastReceiverId;
 782                    }
 783
 0784                    user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 0785                    user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 0786                    user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 0787                    user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 788
 0789                    dbContext.Update(user);
 0790                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 791                }
 0792            }
 0793        }
 794
 795        /// <inheritdoc/>
 796        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 797        {
 0798            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 799            {
 0800                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0801                await using (dbContext.ConfigureAwait(false))
 802                {
 0803                    var user = UserQuery(dbContext)
 0804                        .AsTracking()
 0805                        .FirstOrDefault(u => u.Id.Equals(userId))
 0806                        ?? throw new ArgumentException("No user exists with given Id!");
 807
 808                    // The default number of login attempts is 3, but for some god forsaken reason it's sent to the serv
 0809                    int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 0810                    {
 0811                        -1 => null,
 0812                        0 => 3,
 0813                        _ => policy.LoginAttemptsBeforeLockout
 0814                    };
 815
 0816                    user.MaxParentalRatingScore = policy.MaxParentalRating;
 0817                    user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 0818                    user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 0819                    user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 0820                    user.AuthenticationProviderId = policy.AuthenticationProviderId;
 0821                    user.PasswordResetProviderId = policy.PasswordResetProviderId;
 0822                    user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 0823                    user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 0824                    user.MaxActiveSessions = policy.MaxActiveSessions;
 0825                    user.SyncPlayAccess = policy.SyncPlayAccess;
 0826                    user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 0827                    user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 0828                    user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 0829                    user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 0830                    user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 0831                    user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 0832                    user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 0833                    user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 0834                    user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscod
 0835                    user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscod
 0836                    user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 0837                    user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 0838                    user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 0839                    user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 0840                    user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 0841                    user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 0842                    user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 0843                    user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOther
 0844                    user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 0845                    user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 0846                    user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 0847                    user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 0848                    user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding)
 0849                    user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 850
 0851                    user.AccessSchedules.Clear();
 0852                    foreach (var policyAccessSchedule in policy.AccessSchedules)
 853                    {
 0854                        user.AccessSchedules.Add(policyAccessSchedule);
 855                    }
 856
 857                    // TODO: fix this at some point
 0858                    user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<Unrated
 0859                    user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 0860                    user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 0861                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 0862                    user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 0863                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 0864                    user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFrom
 865
 0866                    dbContext.Update(user);
 0867                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 868                }
 0869            }
 0870        }
 871
 872        /// <inheritdoc/>
 873        public async Task ClearProfileImageAsync(User user)
 874        {
 0875            if (user.ProfileImage is null)
 876            {
 0877                return;
 878            }
 879
 0880            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 881            {
 0882                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0883                await using (dbContext.ConfigureAwait(false))
 884                {
 0885                    dbContext.Remove(user.ProfileImage);
 0886                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 887                }
 888
 0889                user.ProfileImage = null;
 0890            }
 0891        }
 892
 893        internal static void ThrowIfInvalidUsername(string name)
 894        {
 58895            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
 896            {
 50897                return;
 898            }
 899
 8900            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 901        }
 902
 903        private IAuthenticationProvider GetAuthenticationProvider(User user)
 904        {
 3905            return GetAuthenticationProviders(user)[0];
 906        }
 907
 908        private IPasswordResetProvider GetPasswordResetProvider(User? user)
 909        {
 0910            if (user is null)
 911            {
 0912                return _defaultPasswordResetProvider;
 913            }
 914
 0915            return GetPasswordResetProviders(user)[0];
 916        }
 917
 918        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 919        {
 18920            var authenticationProviderId = user?.AuthenticationProviderId;
 921
 18922            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 923
 18924            if (!string.IsNullOrEmpty(authenticationProviderId))
 925            {
 18926                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 927            }
 928
 18929            if (providers.Count == 0)
 930            {
 931                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0932                _logger.LogWarning(
 0933                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0934                    user?.Username,
 0935                    user?.AuthenticationProviderId);
 0936                providers = new List<IAuthenticationProvider>
 0937                {
 0938                    _invalidAuthProvider
 0939                };
 940            }
 941
 18942            return providers;
 943        }
 944
 945        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 946        {
 0947            var passwordResetProviderId = user.PasswordResetProviderId;
 0948            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 949
 0950            if (!string.IsNullOrEmpty(passwordResetProviderId))
 951            {
 0952                providers = providers.Where(i =>
 0953                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0954                    .ToArray();
 955            }
 956
 0957            if (providers.Length == 0)
 958            {
 0959                providers = new IPasswordResetProvider[]
 0960                {
 0961                    _defaultPasswordResetProvider
 0962                };
 963            }
 964
 0965            return providers;
 966        }
 967
 968        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 969                string username,
 970                string password,
 971                User? user)
 972        {
 15973            bool success = false;
 15974            IAuthenticationProvider? authenticationProvider = null;
 975
 45976            foreach (var provider in GetAuthenticationProviders(user))
 977            {
 15978                var providerAuthResult =
 15979                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 15980                var updatedUsername = providerAuthResult.Username;
 15981                success = providerAuthResult.Success;
 982
 15983                if (success)
 984                {
 15985                    authenticationProvider = provider;
 15986                    username = updatedUsername;
 15987                    break;
 988                }
 0989            }
 990
 15991            return (authenticationProvider, username, success);
 15992        }
 993
 994        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 995            IAuthenticationProvider provider,
 996            string username,
 997            string password,
 998            User? resolvedUser)
 999        {
 1000            try
 1001            {
 151002                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 151003                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 151004                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 1005
 151006                if (authenticationResult.Username != username)
 1007                {
 01008                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 01009                    username = authenticationResult.Username;
 1010                }
 1011
 151012                return (username, true);
 1013            }
 01014            catch (AuthenticationException ex)
 1015            {
 01016                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 1017
 01018                return (username, false);
 1019            }
 151020        }
 1021
 1022        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 1023        {
 51024            dbContext.Users.Attach(user);
 51025            dbContext.Entry(user).State = EntityState.Modified;
 51026            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 51027        }
 1028
 1029        /// <inheritdoc/>
 1030        public void Dispose()
 1031        {
 461032            Dispose(true);
 461033            GC.SuppressFinalize(this);
 461034        }
 1035
 1036        /// <summary>
 1037        /// Disposes all members of this class.
 1038        /// </summary>
 1039        /// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
 1040        protected virtual void Dispose(bool disposing)
 1041        {
 461042            if (disposing)
 1043            {
 461044                _userLock.Dispose();
 1045            }
 461046        }
 1047
 1048        internal sealed class LockHelper : IDisposable
 1049        {
 501050            private readonly AsyncKeyedLocker<Guid> _userLock = new();
 1051
 1052            private bool _disposed;
 1053
 21054            public static AsyncLocal<int> IsNestedLock { get; set; } = new();
 1055
 1056            public bool ShouldLock()
 1057            {
 51058                return IsNestedLock.Value == 0;
 1059            }
 1060
 1061            public ValueTask<IDisposable> LockAsync(Guid key)
 1062            {
 481063                ThrowIfDisposed();
 471064                var isNested = LockHelper.IsNestedLock.Value != 0;
 471065                LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value + 1;
 471066                if (isNested)
 1067                {
 11068                    return new ValueTask<IDisposable>(new LockHandle { Parent = null });
 1069                }
 1070
 461071                return AcquireLockAsync(key);
 1072            }
 1073
 1074            private async ValueTask<IDisposable> AcquireLockAsync(Guid key)
 1075            {
 461076                var lockHandle = await _userLock.LockAsync(key, true).ConfigureAwait(false);
 461077                return new LockHandle { Parent = lockHandle };
 461078            }
 1079
 1080            public void Dispose()
 1081            {
 531082                if (_disposed)
 1083                {
 31084                    return;
 1085                }
 1086
 501087                _disposed = true;
 501088                _userLock.Dispose();
 501089            }
 1090
 1091            private void ThrowIfDisposed()
 1092            {
 481093                ObjectDisposedException.ThrowIf(_disposed, this);
 471094            }
 1095
 1096            private sealed class LockHandle : IDisposable
 1097            {
 1098                public required IDisposable? Parent { get; init; }
 1099
 1100                public void Dispose()
 1101                {
 471102                    Parent?.Dispose();
 471103                    LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value - 1;
 1104
 471105                    if (LockHelper.IsNestedLock.Value < 0)
 1106                    {
 01107                        throw new InvalidOperationException("Mismatched locking detected. Threads internal NestedLock is
 1108                    }
 471109                }
 1110            }
 1111        }
 1112    }
 1113}

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()