< 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
61%
Covered lines: 376
Uncovered lines: 231
Coverable lines: 607
Total lines: 1132
Line coverage: 61.9%
Branch coverage
55%
Covered branches: 113
Total branches: 202
Branch coverage: 55.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/2/2026 - 12:12:50 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: 11147/21/2026 - 12:16:33 AM Line coverage: 61.9% (376/607) Branch coverage: 55% (109/198) Total lines: 11327/22/2026 - 12:16:22 AM Line coverage: 61.9% (376/607) Branch coverage: 55.9% (113/202) Total lines: 1132 5/2/2026 - 12:12:50 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: 11147/21/2026 - 12:16:33 AM Line coverage: 61.9% (376/607) Branch coverage: 55% (109/198) Total lines: 11327/22/2026 - 12:16:22 AM Line coverage: 61.9% (376/607) Branch coverage: 55.9% (113/202) Total lines: 1132

Coverage delta

Coverage delta 6 -6

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()68.75%181681.25%
CreateUserInternalAsync()100%22100%
CreateUserAsync()100%22100%
DeleteUserAsync()0%7280%
ResetPassword(...)100%210%
ChangePassword()66.66%6693.75%
GetUserDto(...)57.14%1414100%
AuthenticateUser()63.79%7355841.41%
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()87.5%88100%
ThrowIfInvalidUsername(...)100%88100%
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
 4954        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        {
 4979            _dbProvider = dbProvider;
 4980            _eventManager = eventManager;
 4981            _networkManager = networkManager;
 4982            _appHost = appHost;
 4983            _imageProcessor = imageProcessor;
 4984            _logger = logger;
 4985            _serverConfigurationManager = serverConfigurationManager;
 86
 4987            _passwordResetProviders = passwordResetProviders.ToList();
 4988            _authenticationProviders = authenticationProviders.ToList();
 89
 4990            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 4991            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 4992            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 4993        }
 94
 95        /// <inheritdoc/>
 96        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 97
 98        /// <inheritdoc/>
 99        public IEnumerable<User> GetUsers()
 100        {
 25101            using var dbContext = _dbProvider.CreateDbContext();
 25102            return UserQuery(dbContext)
 25103                .ToArray();
 25104        }
 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        {
 411125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 411130            using var dbContext = _dbProvider.CreateDbContext();
 411131            return UserQuery(dbContext)
 411132                .FirstOrDefault(user => user.Id == id);
 411133        }
 134
 135        private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
 136        {
 550137            return dbContext.Users
 550138                            .AsSingleQuery()
 550139                            .Include(user => user.Permissions)
 550140                            .Include(user => user.Preferences)
 550141                            .Include(user => user.AccessSchedules)
 550142                            .Include(user => user.ProfileImage)
 550143                            .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        {
 2215            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 216            {
 2217                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2218                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
 2221                    var dbUser = await UserQuery(dbContext)
 2222                        .AsTracking()
 2223                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 2224                        .ConfigureAwait(false)
 2225                        ?? throw new ResourceNotFoundException(nameof(user.Id));
 226
 2227                    dbContext.Entry(dbUser).CurrentValues.SetValues(user);
 2228                    dbUser.Permissions.Clear();
 100229                    foreach (var permission in user.Permissions)
 230                    {
 48231                        dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value));
 232                    }
 233
 2234                    dbUser.Preferences.Clear();
 56235                    foreach (var preference in user.Preferences)
 236                    {
 26237                        dbUser.Preferences.Add(new Preference(preference.Kind, preference.Value));
 238                    }
 239
 2240                    dbUser.AccessSchedules.Clear();
 4241                    foreach (var accessSchedule in user.AccessSchedules)
 242                    {
 0243                        dbUser.AccessSchedules.Add(new AccessSchedule(accessSchedule.DayOfWeek, accessSchedule.StartHour
 244                    }
 245
 2246                    if (user.ProfileImage is null)
 247                    {
 1248                        if (dbUser.ProfileImage is not null)
 249                        {
 0250                            dbContext.Remove(dbUser.ProfileImage);
 0251                            dbUser.ProfileImage = null;
 252                        }
 253                    }
 1254                    else if (dbUser.ProfileImage is null)
 255                    {
 1256                        dbUser.ProfileImage = new Jellyfin.Database.Implementations.Entities.ImageInfo(user.ProfileImage
 1257                        {
 1258                            LastModified = user.ProfileImage.LastModified
 1259                        };
 260                    }
 261                    else
 262                    {
 0263                        dbUser.ProfileImage.Path = user.ProfileImage.Path;
 0264                        dbUser.ProfileImage.LastModified = user.ProfileImage.LastModified;
 265                    }
 266
 2267                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 268                }
 2269            }
 2270        }
 271
 272        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 273        {
 274            // TODO: Remove after user item data is migrated.
 50275            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 50276                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 50277                : 0;
 278
 50279            var user = new User(
 50280                name,
 50281                _defaultAuthenticationProvider.GetType().FullName!,
 50282                _defaultPasswordResetProvider.GetType().FullName!)
 50283            {
 50284                InternalId = max + 1
 50285            };
 286
 50287            user.AddDefaultPermissions();
 50288            user.AddDefaultPreferences();
 289
 50290            return user;
 50291        }
 292
 293        /// <inheritdoc/>
 294        public async Task<User> CreateUserAsync(string name)
 295        {
 38296            ThrowIfInvalidUsername(name);
 297
 298            User newUser;
 37299            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 37300            await using (dbContext.ConfigureAwait(false))
 301            {
 302#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 37303                if (await dbContext.Users
 37304                        .AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
 37305                        .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
 33314                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 315
 33316                dbContext.Users.Add(newUser);
 33317                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 318            }
 319
 33320            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 321
 33322            return newUser;
 33323        }
 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                        // ExecuteUpdateAsync bypasses the change tracker, so keep the
 621                        // returned entity in sync. Otherwise SessionManager.LogSessionActivity
 622                        // saves this (stale) entity in full and reverts LastLoginDate.
 16623                        user.LastActivityDate = date;
 16624                        user.LastLoginDate = date;
 16625                    }
 626
 16627                    await dbContext.Users
 16628                        .Where(e => e.Id == user.Id)
 16629                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, 0))
 16630                        .ConfigureAwait(false);
 16631                    _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 632                }
 633                else
 634                {
 0635                    user.InvalidLoginAttemptCount++;
 0636                    int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 0637                    if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 638                    {
 0639                        user.SetPermission(PermissionKind.IsDisabled, true);
 0640                        dbContext.Update(user);
 0641                        await dbContext.SaveChangesAsync()
 0642                            .ConfigureAwait(false);
 0643                        await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 0644                        _logger.LogWarning(
 0645                            "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 0646                            user.Username,
 0647                            user.InvalidLoginAttemptCount);
 648                    }
 649
 0650                    await dbContext.Users
 0651                        .Where(e => e.Id == user.Id)
 0652                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, f => f.InvalidLoginAttem
 0653                        .ConfigureAwait(false);
 654
 0655                    _logger.LogInformation(
 0656                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0657                        user.Username,
 0658                        remoteEndPoint);
 659                }
 16660            }
 661
 16662            return success ? user : null;
 16663        }
 664
 665        /// <inheritdoc/>
 666        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 667        {
 0668            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 0669            var passwordResetProvider = GetPasswordResetProvider(user);
 670
 0671            var result = await passwordResetProvider
 0672                .StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
 0673                .ConfigureAwait(false);
 674
 0675            if (user is not null && isInNetwork)
 676            {
 0677                await UpdateUserAsync(user).ConfigureAwait(false);
 678            }
 679
 0680            return result;
 0681        }
 682
 683        /// <inheritdoc/>
 684        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 685        {
 0686            foreach (var provider in _passwordResetProviders)
 687            {
 0688                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 689
 0690                if (result.Success)
 691                {
 0692                    return result;
 693                }
 694            }
 695
 0696            return new PinRedeemResult();
 0697        }
 698
 699        /// <inheritdoc />
 700        public async Task InitializeAsync()
 701        {
 702            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 18703            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 18704            await using (dbContext.ConfigureAwait(false))
 705            {
 18706                if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
 707                {
 708                    return;
 709                }
 710
 17711                var defaultName = Environment.UserName;
 17712                if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 713                {
 0714                    defaultName = "MyJellyfinUser";
 715                }
 716
 17717                _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 718
 17719                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 17720                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 17721                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 17722                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 723
 17724                dbContext.Users.Add(newUser);
 17725                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 726            }
 18727        }
 728
 729        /// <inheritdoc/>
 730        public NameIdPair[] GetAuthenticationProviders()
 731        {
 0732            return _authenticationProviders
 0733                .Where(provider => provider.IsEnabled)
 0734                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0735                .ThenBy(i => i.Name)
 0736                .Select(i => new NameIdPair
 0737                {
 0738                    Name = i.Name,
 0739                    Id = i.GetType().FullName
 0740                })
 0741                .ToArray();
 742        }
 743
 744        /// <inheritdoc/>
 745        public NameIdPair[] GetPasswordResetProviders()
 746        {
 0747            return _passwordResetProviders
 0748                .Where(provider => provider.IsEnabled)
 0749                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0750                .ThenBy(i => i.Name)
 0751                .Select(i => new NameIdPair
 0752                {
 0753                    Name = i.Name,
 0754                    Id = i.GetType().FullName
 0755                })
 0756                .ToArray();
 757        }
 758
 759        /// <inheritdoc/>
 760        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 761        {
 1762            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 763            {
 1764                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1765                await using (dbContext.ConfigureAwait(false))
 766                {
 1767                    var user = UserQuery(dbContext)
 1768                                   .AsTracking()
 1769                                   .FirstOrDefault(u => u.Id.Equals(userId))
 1770                               ?? throw new ArgumentException("No user exists with given Id!");
 771
 1772                    user.SubtitleMode = config.SubtitleMode;
 1773                    user.HidePlayedInLatest = config.HidePlayedInLatest;
 1774                    user.EnableLocalPassword = config.EnableLocalPassword;
 1775                    user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 1776                    user.DisplayCollectionsView = config.DisplayCollectionsView;
 1777                    user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 1778                    user.AudioLanguagePreference = config.AudioLanguagePreference;
 1779                    user.RememberAudioSelections = config.RememberAudioSelections;
 1780                    user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 1781                    user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 1782                    user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 783
 784                    // Only set cast receiver id if it is passed in and it exists in the server config.
 1785                    if (!string.IsNullOrEmpty(config.CastReceiverId)
 1786                        && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.I
 787                    {
 1788                        user.CastReceiverId = config.CastReceiverId;
 789                    }
 790
 1791                    user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 1792                    user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 1793                    user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 1794                    user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 795
 1796                    dbContext.Update(user);
 1797                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 798                }
 1799            }
 1800        }
 801
 802        /// <inheritdoc/>
 803        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 804        {
 0805            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 806            {
 0807                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0808                await using (dbContext.ConfigureAwait(false))
 809                {
 0810                    var user = UserQuery(dbContext)
 0811                        .AsTracking()
 0812                        .FirstOrDefault(u => u.Id.Equals(userId))
 0813                        ?? throw new ArgumentException("No user exists with given Id!");
 814
 815                    // The default number of login attempts is 3, but for some god forsaken reason it's sent to the serv
 0816                    int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 0817                    {
 0818                        -1 => null,
 0819                        0 => 3,
 0820                        _ => policy.LoginAttemptsBeforeLockout
 0821                    };
 822
 0823                    user.MaxParentalRatingScore = policy.MaxParentalRating;
 0824                    user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 0825                    user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 0826                    user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 0827                    user.AuthenticationProviderId = policy.AuthenticationProviderId;
 0828                    user.PasswordResetProviderId = policy.PasswordResetProviderId;
 0829                    user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 0830                    user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 0831                    user.MaxActiveSessions = policy.MaxActiveSessions;
 0832                    user.SyncPlayAccess = policy.SyncPlayAccess;
 0833                    user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 0834                    user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 0835                    user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 0836                    user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 0837                    user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 0838                    user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 0839                    user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 0840                    user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 0841                    user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscod
 0842                    user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscod
 0843                    user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 0844                    user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 0845                    user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 0846                    user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 0847                    user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 0848                    user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 0849                    user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 0850                    user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOther
 0851                    user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 0852                    user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 0853                    user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 0854                    user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 0855                    user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding)
 0856                    user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 857
 0858                    user.AccessSchedules.Clear();
 0859                    foreach (var policyAccessSchedule in policy.AccessSchedules)
 860                    {
 0861                        user.AccessSchedules.Add(policyAccessSchedule);
 862                    }
 863
 864                    // TODO: fix this at some point
 0865                    user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<Unrated
 0866                    user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 0867                    user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 0868                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 0869                    user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 0870                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 0871                    user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFrom
 872
 0873                    dbContext.Update(user);
 0874                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 875                }
 0876            }
 0877        }
 878
 879        /// <inheritdoc/>
 880        public async Task ClearProfileImageAsync(User user)
 881        {
 2882            if (user.ProfileImage is null)
 883            {
 1884                return;
 885            }
 886
 1887            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 888            {
 1889                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1890                await using (dbContext.ConfigureAwait(false))
 891                {
 892                    // Remove the tracked profile image loaded from the database instead of the
 893                    // detached instance on the passed in user. That instance can carry a stale,
 894                    // never-persisted (temporary) key, which makes EF Core throw when it is marked
 895                    // for deletion, leaving the profile image impossible to clear or replace.
 1896                    var dbUser = await UserQuery(dbContext)
 1897                        .AsTracking()
 1898                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 1899                        .ConfigureAwait(false);
 1900                    if (dbUser?.ProfileImage is not null)
 901                    {
 1902                        dbContext.Remove(dbUser.ProfileImage);
 1903                        dbUser.ProfileImage = null;
 1904                        await dbContext.SaveChangesAsync().ConfigureAwait(false);
 905                    }
 906                }
 907
 1908                user.ProfileImage = null;
 1909            }
 2910        }
 911
 912        internal static void ThrowIfInvalidUsername(string name)
 913        {
 63914            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name) && !string.Equals(name, ".", Stri
 915            {
 53916                return;
 917            }
 918
 10919            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 920        }
 921
 922        private IAuthenticationProvider GetAuthenticationProvider(User user)
 923        {
 3924            return GetAuthenticationProviders(user)[0];
 925        }
 926
 927        private IPasswordResetProvider GetPasswordResetProvider(User? user)
 928        {
 0929            if (user is null)
 930            {
 0931                return _defaultPasswordResetProvider;
 932            }
 933
 0934            return GetPasswordResetProviders(user)[0];
 935        }
 936
 937        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 938        {
 19939            var authenticationProviderId = user?.AuthenticationProviderId;
 940
 19941            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 942
 19943            if (!string.IsNullOrEmpty(authenticationProviderId))
 944            {
 19945                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 946            }
 947
 19948            if (providers.Count == 0)
 949            {
 950                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0951                _logger.LogWarning(
 0952                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0953                    user?.Username,
 0954                    user?.AuthenticationProviderId);
 0955                providers = new List<IAuthenticationProvider>
 0956                {
 0957                    _invalidAuthProvider
 0958                };
 959            }
 960
 19961            return providers;
 962        }
 963
 964        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 965        {
 0966            var passwordResetProviderId = user.PasswordResetProviderId;
 0967            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 968
 0969            if (!string.IsNullOrEmpty(passwordResetProviderId))
 970            {
 0971                providers = providers.Where(i =>
 0972                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0973                    .ToArray();
 974            }
 975
 0976            if (providers.Length == 0)
 977            {
 0978                providers = new IPasswordResetProvider[]
 0979                {
 0980                    _defaultPasswordResetProvider
 0981                };
 982            }
 983
 0984            return providers;
 985        }
 986
 987        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 988                string username,
 989                string password,
 990                User? user)
 991        {
 16992            bool success = false;
 16993            IAuthenticationProvider? authenticationProvider = null;
 994
 48995            foreach (var provider in GetAuthenticationProviders(user))
 996            {
 16997                var providerAuthResult =
 16998                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 16999                var updatedUsername = providerAuthResult.Username;
 161000                success = providerAuthResult.Success;
 1001
 161002                if (success)
 1003                {
 161004                    authenticationProvider = provider;
 161005                    username = updatedUsername;
 161006                    break;
 1007                }
 01008            }
 1009
 161010            return (authenticationProvider, username, success);
 161011        }
 1012
 1013        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 1014            IAuthenticationProvider provider,
 1015            string username,
 1016            string password,
 1017            User? resolvedUser)
 1018        {
 1019            try
 1020            {
 161021                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 161022                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 161023                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 1024
 161025                if (authenticationResult.Username != username)
 1026                {
 01027                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 01028                    username = authenticationResult.Username;
 1029                }
 1030
 161031                return (username, true);
 1032            }
 01033            catch (AuthenticationException ex)
 1034            {
 01035                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 1036
 01037                return (username, false);
 1038            }
 161039        }
 1040
 1041        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 1042        {
 61043            dbContext.Users.Attach(user);
 61044            dbContext.Entry(user).State = EntityState.Modified;
 61045            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 61046        }
 1047
 1048        /// <inheritdoc/>
 1049        public void Dispose()
 1050        {
 491051            Dispose(true);
 491052            GC.SuppressFinalize(this);
 491053        }
 1054
 1055        /// <summary>
 1056        /// Disposes all members of this class.
 1057        /// </summary>
 1058        /// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
 1059        protected virtual void Dispose(bool disposing)
 1060        {
 491061            if (disposing)
 1062            {
 491063                _userLock.Dispose();
 1064            }
 491065        }
 1066
 1067        internal sealed class LockHelper : IDisposable
 1068        {
 531069            private readonly AsyncKeyedLocker<Guid> _userLock = new();
 1070
 1071            private bool _disposed;
 1072
 21073            public static AsyncLocal<int> IsNestedLock { get; set; } = new();
 1074
 1075            public bool ShouldLock()
 1076            {
 51077                return IsNestedLock.Value == 0;
 1078            }
 1079
 1080            public ValueTask<IDisposable> LockAsync(Guid key)
 1081            {
 381082                ThrowIfDisposed();
 371083                var isNested = LockHelper.IsNestedLock.Value != 0;
 371084                LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value + 1;
 371085                if (isNested)
 1086                {
 11087                    return new ValueTask<IDisposable>(new LockHandle { Parent = null });
 1088                }
 1089
 361090                return AcquireLockAsync(key);
 1091            }
 1092
 1093            private async ValueTask<IDisposable> AcquireLockAsync(Guid key)
 1094            {
 361095                var lockHandle = await _userLock.LockAsync(key, true).ConfigureAwait(false);
 361096                return new LockHandle { Parent = lockHandle };
 361097            }
 1098
 1099            public void Dispose()
 1100            {
 561101                if (_disposed)
 1102                {
 31103                    return;
 1104                }
 1105
 531106                _disposed = true;
 531107                _userLock.Dispose();
 531108            }
 1109
 1110            private void ThrowIfDisposed()
 1111            {
 381112                ObjectDisposedException.ThrowIf(_disposed, this);
 371113            }
 1114
 1115            private sealed class LockHandle : IDisposable
 1116            {
 1117                public required IDisposable? Parent { get; init; }
 1118
 1119                public void Dispose()
 1120                {
 371121                    Parent?.Dispose();
 371122                    LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value - 1;
 1123
 371124                    if (LockHelper.IsNestedLock.Value < 0)
 1125                    {
 01126                        throw new InvalidOperationException("Mismatched locking detected. Threads internal NestedLock is
 1127                    }
 371128                }
 1129            }
 1130        }
 1131    }
 1132}

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