< 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
71%
Covered lines: 451
Uncovered lines: 176
Coverable lines: 627
Total lines: 1183
Line coverage: 71.9%
Branch coverage
59%
Covered branches: 128
Total branches: 214
Branch coverage: 59.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 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: 113210/29/2026 - 12:17:11 AM Line coverage: 71.9% (451/627) Branch coverage: 59.8% (128/214) Total lines: 1183 5/16/2026 - 12:15:55 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: 113210/29/2026 - 12:17:11 AM Line coverage: 71.9% (451/627) Branch coverage: 59.8% (128/214) Total lines: 1183

Coverage delta

Coverage delta 10 -10

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()66.66%131282.14%
SyncPermissions(...)87.5%8890%
SyncPreferences(...)75%9880%
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()41.66%121295.38%
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
 5854        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        {
 5879            _dbProvider = dbProvider;
 5880            _eventManager = eventManager;
 5881            _networkManager = networkManager;
 5882            _appHost = appHost;
 5883            _imageProcessor = imageProcessor;
 5884            _logger = logger;
 5885            _serverConfigurationManager = serverConfigurationManager;
 86
 5887            _passwordResetProviders = passwordResetProviders.ToList();
 5888            _authenticationProviders = authenticationProviders.ToList();
 89
 5890            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 5891            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 5892            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 5893        }
 94
 95        /// <inheritdoc/>
 96        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 97
 98        /// <inheritdoc/>
 99        public IEnumerable<User> GetUsers()
 100        {
 31101            using var dbContext = _dbProvider.CreateDbContext();
 31102            return UserQuery(dbContext)
 31103                .ToArray();
 31104        }
 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        {
 460125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 460130            using var dbContext = _dbProvider.CreateDbContext();
 460131            return UserQuery(dbContext)
 460132                .FirstOrDefault(user => user.Id == id);
 460133        }
 134
 135        private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
 136        {
 625137            return dbContext.Users
 625138                            .AsSingleQuery()
 625139                            .Include(user => user.Permissions)
 625140                            .Include(user => user.Preferences)
 625141                            .Include(user => user.AccessSchedules)
 625142                            .Include(user => user.ProfileImage)
 625143                            .AsNoTracking();
 144        }
 145
 146        /// <inheritdoc/>
 147        public User? GetFirstUser()
 148        {
 23149            using var dbContext = _dbProvider.CreateDbContext();
 23150            return UserQuery(dbContext).FirstOrDefault();
 23151        }
 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        {
 12171            ThrowIfInvalidUsername(newName);
 172
 12173            if (oldName.Equals(newName, StringComparison.Ordinal))
 174            {
 0175                throw new ArgumentException("The new and old names must be different.");
 176            }
 177
 12178            User user = null!; // user is never actually null where its used afterwards so we can just ignore.
 12179            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 180            {
 12181                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 12182                await using (dbContext.ConfigureAwait(false))
 183                {
 184#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 12185                    if (await dbContext.Users
 12186                            .AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
 12187                            .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
 8196                    user = await UserQuery(dbContext)
 8197                        .AsTracking()
 8198                        .FirstOrDefaultAsync(u => u.Id == userId)
 8199                        .ConfigureAwait(false)
 8200                        ?? throw new ResourceNotFoundException(nameof(userId));
 8201                    user.Username = newName;
 8202                    user.NormalizedUsername = newName.ToUpperInvariant();
 8203                    await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 204                }
 8205            }
 206
 8207            var eventArgs = new UserUpdatedEventArgs(user);
 8208            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 8209            OnUserUpdated?.Invoke(this, eventArgs);
 8210        }
 211
 212        /// <inheritdoc/>
 213        public async Task UpdateUserAsync(User user)
 214        {
 13215            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 216            {
 13217                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 13218                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
 13221                    var dbUser = await UserQuery(dbContext)
 13222                        .AsTracking()
 13223                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 13224                        .ConfigureAwait(false)
 13225                        ?? throw new ResourceNotFoundException(nameof(user.Id));
 226
 13227                    dbContext.Entry(dbUser).CurrentValues.SetValues(user);
 13228                    SyncPermissions(dbUser, user.Permissions);
 13229                    SyncPreferences(dbUser, user.Preferences);
 230
 13231                    dbUser.AccessSchedules.Clear();
 26232                    foreach (var accessSchedule in user.AccessSchedules)
 233                    {
 0234                        dbUser.AccessSchedules.Add(new AccessSchedule(accessSchedule.DayOfWeek, accessSchedule.StartHour
 235                    }
 236
 13237                    if (user.ProfileImage is null)
 238                    {
 12239                        if (dbUser.ProfileImage is not null)
 240                        {
 0241                            dbContext.Remove(dbUser.ProfileImage);
 0242                            dbUser.ProfileImage = null;
 243                        }
 244                    }
 1245                    else if (dbUser.ProfileImage is null)
 246                    {
 1247                        dbUser.ProfileImage = new Jellyfin.Database.Implementations.Entities.ImageInfo(user.ProfileImage
 1248                        {
 1249                            LastModified = user.ProfileImage.LastModified
 1250                        };
 251                    }
 252                    else
 253                    {
 0254                        dbUser.ProfileImage.Path = user.ProfileImage.Path;
 0255                        dbUser.ProfileImage.LastModified = user.ProfileImage.LastModified;
 256                    }
 257
 13258                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 259                }
 13260            }
 13261        }
 262
 263        private static void SyncPermissions(User dbUser, ICollection<Permission> source)
 264        {
 13265            var incoming = new Dictionary<PermissionKind, bool>();
 648266            foreach (var permission in source)
 267            {
 311268                incoming[permission.Kind] = permission.Value;
 269            }
 270
 650271            foreach (var existing in dbUser.Permissions)
 272            {
 312273                if (incoming.Remove(existing.Kind, out var value))
 274                {
 275                    // EF only marks the row modified if the value actually differs, so an update that
 276                    // touches nothing but the user row - a session activity stamp - writes no children.
 311277                    existing.Value = value;
 278                }
 279                else
 280                {
 1281                    dbUser.Permissions.Remove(existing);
 282                }
 283            }
 284
 26285            foreach (var (kind, value) in incoming)
 286            {
 0287                dbUser.Permissions.Add(new Permission(kind, value));
 288            }
 13289        }
 290
 291        private static void SyncPreferences(User dbUser, ICollection<Preference> source)
 292        {
 13293            var incoming = new Dictionary<PreferenceKind, string>();
 364294            foreach (var preference in source)
 295            {
 169296                incoming[preference.Kind] = preference.Value;
 297            }
 298
 364299            foreach (var existing in dbUser.Preferences)
 300            {
 169301                if (incoming.Remove(existing.Kind, out var value))
 302                {
 169303                    existing.Value = value;
 304                }
 305                else
 306                {
 0307                    dbUser.Preferences.Remove(existing);
 308                }
 309            }
 310
 26311            foreach (var (kind, value) in incoming)
 312            {
 0313                dbUser.Preferences.Add(new Preference(kind, value));
 314            }
 13315        }
 316
 317        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 318        {
 319            // TODO: Remove after user item data is migrated.
 58320            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 58321                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 58322                : 0;
 323
 58324            var user = new User(
 58325                name,
 58326                _defaultAuthenticationProvider.GetType().FullName!,
 58327                _defaultPasswordResetProvider.GetType().FullName!)
 58328            {
 58329                InternalId = max + 1
 58330            };
 331
 58332            user.AddDefaultPermissions();
 58333            user.AddDefaultPreferences();
 334
 58335            return user;
 58336        }
 337
 338        /// <inheritdoc/>
 339        public async Task<User> CreateUserAsync(string name)
 340        {
 44341            ThrowIfInvalidUsername(name);
 342
 343            User newUser;
 43344            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 43345            await using (dbContext.ConfigureAwait(false))
 346            {
 347#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 43348                if (await dbContext.Users
 43349                        .AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
 43350                        .ConfigureAwait(false))
 351                {
 4352                    throw new ArgumentException(string.Format(
 4353                        CultureInfo.InvariantCulture,
 4354                        "A user with the name '{0}' already exists.",
 4355                        name));
 356                }
 357#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 358
 39359                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 360
 39361                dbContext.Users.Add(newUser);
 39362                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 363            }
 364
 39365            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 366
 39367            return newUser;
 39368        }
 369
 370        /// <inheritdoc/>
 371        public async Task DeleteUserAsync(Guid userId)
 372        {
 373            User? user;
 0374            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 375            {
 0376                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0377                await using (dbContext.ConfigureAwait(false))
 378                {
 0379                    user = await dbContext.Users
 0380                        .Include(u => u.Permissions)
 0381                        .FirstOrDefaultAsync(u => u.Id.Equals(userId))
 0382                        .ConfigureAwait(false);
 0383                    if (user is null)
 384                    {
 0385                        throw new ResourceNotFoundException(nameof(userId));
 386                    }
 387
 0388                    var userCount = await dbContext.Users.CountAsync().ConfigureAwait(false);
 0389                    if (userCount == 1)
 390                    {
 0391                        throw new InvalidOperationException(string.Format(
 0392                            CultureInfo.InvariantCulture,
 0393                            "The user '{0}' cannot be deleted because there must be at least one user in the system.",
 0394                            user.Username));
 395                    }
 396
 0397                    if (user.HasPermission(PermissionKind.IsAdministrator)
 0398                        && await dbContext.Users
 0399                            .CountAsync(i => i.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value)
 0400                            .ConfigureAwait(false) == 1)
 401                    {
 0402                        throw new ArgumentException(
 0403                            string.Format(
 0404                                CultureInfo.InvariantCulture,
 0405                                "The user '{0}' cannot be deleted because there must be at least one admin user in the s
 0406                                user.Username),
 0407                            nameof(userId));
 408                    }
 409
 0410                    dbContext.Users.Remove(user);
 0411                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 412                }
 0413            }
 414
 0415            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 0416        }
 417
 418        /// <inheritdoc/>
 419        public Task ResetPassword(Guid userId)
 420        {
 0421            return ChangePassword(userId, string.Empty);
 422        }
 423
 424        /// <inheritdoc/>
 425        public async Task ChangePassword(Guid userId, string newPassword)
 426        {
 5427            User dbUser = null!;
 5428            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 429            {
 5430                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 5431                await using (dbContext.ConfigureAwait(false))
 432                {
 5433                    dbUser = await UserQuery(dbContext)
 5434                        .AsTracking()
 5435                        .FirstOrDefaultAsync(u => u.Id == userId)
 5436                        .ConfigureAwait(false)
 5437                        ?? throw new ResourceNotFoundException(nameof(userId));
 5438                    if (dbUser.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
 439                    {
 0440                        throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
 441                    }
 442
 5443                    await GetAuthenticationProvider(dbUser).ChangePassword(dbUser, newPassword).ConfigureAwait(false);
 5444                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 445                }
 5446            }
 447
 5448            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(dbUser)).ConfigureAwait(false);
 5449        }
 450
 451        /// <inheritdoc/>
 452        public UserDto GetUserDto(User user, string? remoteEndPoint = null)
 453        {
 41454            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 41455            return new UserDto
 41456            {
 41457                Name = user.Username,
 41458                Id = user.Id,
 41459                ServerId = _appHost.SystemId,
 41460                EnableAutoLogin = user.EnableAutoLogin,
 41461                LastLoginDate = user.LastLoginDate,
 41462                LastActivityDate = user.LastActivityDate,
 41463                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 41464                Configuration = new UserConfiguration
 41465                {
 41466                    SubtitleMode = user.SubtitleMode,
 41467                    HidePlayedInLatest = user.HidePlayedInLatest,
 41468                    EnableLocalPassword = user.EnableLocalPassword,
 41469                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 41470                    DisplayCollectionsView = user.DisplayCollectionsView,
 41471                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 41472                    AudioLanguagePreference = user.AudioLanguagePreference,
 41473                    RememberAudioSelections = user.RememberAudioSelections,
 41474                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 41475                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 41476                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 41477                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 41478                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 41479                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 41480                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 41481                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 41482                        ? castReceiverApplications.FirstOrDefault()?.Id
 41483                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 41484                          ?? castReceiverApplications.FirstOrDefault()?.Id
 41485                },
 41486                Policy = new UserPolicy
 41487                {
 41488                    MaxParentalRating = user.MaxParentalRatingScore,
 41489                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
 41490                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 41491                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 41492                    AuthenticationProviderId = user.AuthenticationProviderId,
 41493                    PasswordResetProviderId = user.PasswordResetProviderId,
 41494                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 41495                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 41496                    MaxActiveSessions = user.MaxActiveSessions,
 41497                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 41498                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 41499                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 41500                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 41501                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 41502                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 41503                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 41504                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 41505                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 41506                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 41507                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 41508                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 41509                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 41510                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 41511                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 41512                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 41513                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 41514                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 41515                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 41516                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 41517                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 41518                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 41519                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 41520                    AccessSchedules = user.AccessSchedules.ToArray(),
 41521                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 41522                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 41523                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 41524                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 41525                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 41526                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 41527                    SyncPlayAccess = user.SyncPlayAccess,
 41528                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 41529                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 41530                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 41531                }
 41532            };
 533        }
 534
 535        /// <inheritdoc/>
 536        public async Task<User?> AuthenticateUser(
 537            string username,
 538            string password,
 539            string remoteEndPoint,
 540            bool isUserSession)
 541        {
 16542            if (string.IsNullOrWhiteSpace(username))
 543            {
 0544                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndP
 0545                throw new ArgumentNullException(nameof(username));
 546            }
 547
 548            bool success;
 16549            var user = GetUserByName(username);
 16550            using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false))
 551            {
 16552                using var dbContext = _dbProvider.CreateDbContext();
 553
 554                // Reload the user now that we hold the lock so the RowVersion is current.
 555                // GetUserByName uses AsNoTracking and the snapshot may be stale if another
 556                // write (e.g. a concurrent login) incremented RowVersion after our initial load.
 16557                if (user is not null)
 558                {
 16559                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false) ??
 560                }
 561
 16562                var authResult = await AuthenticateLocalUser(username, password, user)
 16563                    .ConfigureAwait(false);
 16564                var authenticationProvider = authResult.AuthenticationProvider;
 16565                success = authResult.Success;
 566
 16567                if (success && user is not null)
 568                {
 569                    // refresh the user if the auth provider might have updated it in the auth method.
 570                    // this is a hack, this needs removal once the LDAP plugin uses the correct interface to get the use
 16571                    user = await UserQuery(dbContext).FirstOrDefaultAsync(e => e.Id == user.Id).ConfigureAwait(false);
 572                }
 573
 16574                if (user is null)
 575                {
 0576                    string updatedUsername = authResult.Username;
 577
 0578                    if (success
 0579                        && authenticationProvider is not null
 0580                        && authenticationProvider is not DefaultAuthenticationProvider)
 581                    {
 582                        // Trust the username returned by the authentication provider
 0583                        username = updatedUsername;
 584
 585                        // Search the database for the user again
 586                        // the authentication provider might have created it
 587#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 0588                        user = await UserQuery(dbContext)
 0589                            .FirstOrDefaultAsync(e => e.NormalizedUsername == username.ToUpperInvariant()).ConfigureAwai
 590
 0591                        if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
 592                        {
 0593                            await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
 0594                            user = await UserQuery(dbContext)
 0595                                .FirstOrDefaultAsync(e => e.NormalizedUsername == username.ToUpperInvariant()).Configure
 596#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 597                        }
 598                    }
 599                }
 600
 16601                if (success && user is not null && authenticationProvider is not null)
 602                {
 16603                    var providerId = authenticationProvider.GetType().FullName;
 604
 16605                    if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringCompar
 606                    {
 0607                        await dbContext.Users
 0608                            .Where(e => e.Id == user.Id)
 0609                            .ExecuteUpdateAsync(e => e.SetProperty(f => f.AuthenticationProviderId, providerId))
 0610                            .ConfigureAwait(false);
 611                    }
 612                }
 613
 16614                if (user is null)
 615                {
 0616                    _logger.LogInformation(
 0617                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0618                        username,
 0619                        remoteEndPoint);
 0620                    throw new AuthenticationException("Invalid username or password entered.");
 621                }
 622
 16623                if (user.HasPermission(PermissionKind.IsDisabled))
 624                {
 0625                    _logger.LogInformation(
 0626                        "Authentication request for {UserName} has been denied because this account is currently disable
 0627                        username,
 0628                        remoteEndPoint);
 0629                    throw new SecurityException(
 0630                        $"The {user.Username} account is currently disabled. Please consult with your administrator.");
 631                }
 632
 16633                if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 16634                    !_networkManager.IsInLocalNetwork(remoteEndPoint))
 635                {
 0636                    _logger.LogInformation(
 0637                        "Authentication request for {UserName} forbidden: remote access disabled and user not in local n
 0638                        username,
 0639                        remoteEndPoint);
 0640                    throw new SecurityException("Forbidden.");
 641                }
 642
 16643                if (!user.IsParentalScheduleAllowed())
 644                {
 0645                    _logger.LogInformation(
 0646                        "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP
 0647                        username,
 0648                        remoteEndPoint);
 0649                    throw new SecurityException("User is not allowed access at this time.");
 650                }
 651
 652                // Update LastActivityDate and LastLoginDate, then save
 16653                if (success)
 654                {
 16655                    if (isUserSession)
 656                    {
 16657                        var date = DateTime.UtcNow;
 16658                        await dbContext.Users
 16659                            .Where(e => e.Id == user.Id)
 16660                            .ExecuteUpdateAsync(e => e
 16661                                .SetProperty(f => f.LastActivityDate, date)
 16662                                .SetProperty(f => f.LastLoginDate, date))
 16663                            .ConfigureAwait(false);
 664
 665                        // ExecuteUpdateAsync bypasses the change tracker, so keep the
 666                        // returned entity in sync. Otherwise SessionManager.LogSessionActivity
 667                        // saves this (stale) entity in full and reverts LastLoginDate.
 16668                        user.LastActivityDate = date;
 16669                        user.LastLoginDate = date;
 16670                    }
 671
 16672                    await dbContext.Users
 16673                        .Where(e => e.Id == user.Id)
 16674                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, 0))
 16675                        .ConfigureAwait(false);
 16676                    _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 677                }
 678                else
 679                {
 0680                    user.InvalidLoginAttemptCount++;
 0681                    int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 0682                    if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 683                    {
 0684                        user.SetPermission(PermissionKind.IsDisabled, true);
 0685                        dbContext.Update(user);
 0686                        await dbContext.SaveChangesAsync()
 0687                            .ConfigureAwait(false);
 0688                        await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 0689                        _logger.LogWarning(
 0690                            "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 0691                            user.Username,
 0692                            user.InvalidLoginAttemptCount);
 693                    }
 694
 0695                    await dbContext.Users
 0696                        .Where(e => e.Id == user.Id)
 0697                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.InvalidLoginAttemptCount, f => f.InvalidLoginAttem
 0698                        .ConfigureAwait(false);
 699
 0700                    _logger.LogInformation(
 0701                        "Authentication request for {UserName} has been denied (IP: {IP}).",
 0702                        user.Username,
 0703                        remoteEndPoint);
 704                }
 16705            }
 706
 16707            return success ? user : null;
 16708        }
 709
 710        /// <inheritdoc/>
 711        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 712        {
 0713            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 0714            var passwordResetProvider = GetPasswordResetProvider(user);
 715
 0716            var result = await passwordResetProvider
 0717                .StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
 0718                .ConfigureAwait(false);
 719
 0720            if (user is not null && isInNetwork)
 721            {
 0722                await UpdateUserAsync(user).ConfigureAwait(false);
 723            }
 724
 0725            return result;
 0726        }
 727
 728        /// <inheritdoc/>
 729        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 730        {
 0731            foreach (var provider in _passwordResetProviders)
 732            {
 0733                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 734
 0735                if (result.Success)
 736                {
 0737                    return result;
 738                }
 739            }
 740
 0741            return new PinRedeemResult();
 0742        }
 743
 744        /// <inheritdoc />
 745        public async Task InitializeAsync()
 746        {
 747            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 20748            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 20749            await using (dbContext.ConfigureAwait(false))
 750            {
 20751                if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
 752                {
 753                    return;
 754                }
 755
 19756                var defaultName = Environment.UserName;
 19757                if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 758                {
 0759                    defaultName = "MyJellyfinUser";
 760                }
 761
 19762                _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 763
 19764                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 19765                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 19766                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 19767                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 768
 19769                dbContext.Users.Add(newUser);
 19770                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 771            }
 20772        }
 773
 774        /// <inheritdoc/>
 775        public NameIdPair[] GetAuthenticationProviders()
 776        {
 0777            return _authenticationProviders
 0778                .Where(provider => provider.IsEnabled)
 0779                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0780                .ThenBy(i => i.Name)
 0781                .Select(i => new NameIdPair
 0782                {
 0783                    Name = i.Name,
 0784                    Id = i.GetType().FullName
 0785                })
 0786                .ToArray();
 787        }
 788
 789        /// <inheritdoc/>
 790        public NameIdPair[] GetPasswordResetProviders()
 791        {
 0792            return _passwordResetProviders
 0793                .Where(provider => provider.IsEnabled)
 0794                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0795                .ThenBy(i => i.Name)
 0796                .Select(i => new NameIdPair
 0797                {
 0798                    Name = i.Name,
 0799                    Id = i.GetType().FullName
 0800                })
 0801                .ToArray();
 802        }
 803
 804        /// <inheritdoc/>
 805        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 806        {
 1807            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 808            {
 1809                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1810                await using (dbContext.ConfigureAwait(false))
 811                {
 1812                    var user = UserQuery(dbContext)
 1813                                   .AsTracking()
 1814                                   .FirstOrDefault(u => u.Id.Equals(userId))
 1815                               ?? throw new ArgumentException("No user exists with given Id!");
 816
 1817                    user.SubtitleMode = config.SubtitleMode;
 1818                    user.HidePlayedInLatest = config.HidePlayedInLatest;
 1819                    user.EnableLocalPassword = config.EnableLocalPassword;
 1820                    user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 1821                    user.DisplayCollectionsView = config.DisplayCollectionsView;
 1822                    user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 1823                    user.AudioLanguagePreference = config.AudioLanguagePreference;
 1824                    user.RememberAudioSelections = config.RememberAudioSelections;
 1825                    user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 1826                    user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 1827                    user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 828
 829                    // Only set cast receiver id if it is passed in and it exists in the server config.
 1830                    if (!string.IsNullOrEmpty(config.CastReceiverId)
 1831                        && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.I
 832                    {
 1833                        user.CastReceiverId = config.CastReceiverId;
 834                    }
 835
 1836                    user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 1837                    user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 1838                    user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 1839                    user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 840
 1841                    dbContext.Update(user);
 1842                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 843                }
 1844            }
 1845        }
 846
 847        /// <inheritdoc/>
 848        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 849        {
 850            User user;
 1851            using (await _userLock.LockAsync(userId).ConfigureAwait(false))
 852            {
 1853                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1854                await using (dbContext.ConfigureAwait(false))
 855                {
 1856                    user = await UserQuery(dbContext)
 1857                        .AsTracking()
 1858                        .FirstOrDefaultAsync(u => u.Id.Equals(userId))
 1859                        .ConfigureAwait(false)
 1860                        ?? throw new ArgumentException("No user exists with given Id!");
 861
 862                    // The default number of login attempts is 3, but for some god forsaken reason it's sent to the serv
 1863                    int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 1864                    {
 1865                        -1 => null,
 0866                        0 => 3,
 0867                        _ => policy.LoginAttemptsBeforeLockout
 1868                    };
 869
 1870                    user.MaxParentalRatingScore = policy.MaxParentalRating;
 1871                    user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 1872                    user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 1873                    user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 1874                    user.AuthenticationProviderId = policy.AuthenticationProviderId;
 1875                    user.PasswordResetProviderId = policy.PasswordResetProviderId;
 1876                    user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 1877                    user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 1878                    user.MaxActiveSessions = policy.MaxActiveSessions;
 1879                    user.SyncPlayAccess = policy.SyncPlayAccess;
 1880                    user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 1881                    user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 1882                    user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 1883                    user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 1884                    user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 1885                    user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 1886                    user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 1887                    user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 1888                    user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscod
 1889                    user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscod
 1890                    user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 1891                    user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 1892                    user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 1893                    user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 1894                    user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 1895                    user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 1896                    user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 1897                    user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOther
 1898                    user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 1899                    user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 1900                    user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 1901                    user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 1902                    user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding)
 1903                    user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 904
 1905                    user.AccessSchedules.Clear();
 2906                    foreach (var policyAccessSchedule in policy.AccessSchedules)
 907                    {
 0908                        user.AccessSchedules.Add(policyAccessSchedule);
 909                    }
 910
 911                    // TODO: fix this at some point
 1912                    user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<Unrated
 1913                    user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 1914                    user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 1915                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 1916                    user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 1917                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 1918                    user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFrom
 919
 1920                    dbContext.Update(user);
 1921                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 922                }
 1923            }
 924
 1925            var eventArgs = new UserUpdatedEventArgs(user);
 1926            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1927            OnUserUpdated?.Invoke(this, eventArgs);
 1928        }
 929
 930        /// <inheritdoc/>
 931        public async Task ClearProfileImageAsync(User user)
 932        {
 2933            if (user.ProfileImage is null)
 934            {
 1935                return;
 936            }
 937
 1938            using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
 939            {
 1940                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 1941                await using (dbContext.ConfigureAwait(false))
 942                {
 943                    // Remove the tracked profile image loaded from the database instead of the
 944                    // detached instance on the passed in user. That instance can carry a stale,
 945                    // never-persisted (temporary) key, which makes EF Core throw when it is marked
 946                    // for deletion, leaving the profile image impossible to clear or replace.
 1947                    var dbUser = await UserQuery(dbContext)
 1948                        .AsTracking()
 1949                        .FirstOrDefaultAsync(u => u.Id == user.Id)
 1950                        .ConfigureAwait(false);
 1951                    if (dbUser?.ProfileImage is not null)
 952                    {
 1953                        dbContext.Remove(dbUser.ProfileImage);
 1954                        dbUser.ProfileImage = null;
 1955                        await dbContext.SaveChangesAsync().ConfigureAwait(false);
 956                    }
 957                }
 958
 1959                user.ProfileImage = null;
 1960            }
 2961        }
 962
 963        internal static void ThrowIfInvalidUsername(string name)
 964        {
 71965            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name) && !string.Equals(name, ".", Stri
 966            {
 61967                return;
 968            }
 969
 10970            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 971        }
 972
 973        private IAuthenticationProvider GetAuthenticationProvider(User user)
 974        {
 5975            return GetAuthenticationProviders(user)[0];
 976        }
 977
 978        private IPasswordResetProvider GetPasswordResetProvider(User? user)
 979        {
 0980            if (user is null)
 981            {
 0982                return _defaultPasswordResetProvider;
 983            }
 984
 0985            return GetPasswordResetProviders(user)[0];
 986        }
 987
 988        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 989        {
 21990            var authenticationProviderId = user?.AuthenticationProviderId;
 991
 21992            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 993
 21994            if (!string.IsNullOrEmpty(authenticationProviderId))
 995            {
 21996                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 997            }
 998
 21999            if (providers.Count == 0)
 1000            {
 1001                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 01002                _logger.LogWarning(
 01003                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 01004                    user?.Username,
 01005                    user?.AuthenticationProviderId);
 01006                providers = new List<IAuthenticationProvider>
 01007                {
 01008                    _invalidAuthProvider
 01009                };
 1010            }
 1011
 211012            return providers;
 1013        }
 1014
 1015        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 1016        {
 01017            var passwordResetProviderId = user.PasswordResetProviderId;
 01018            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 1019
 01020            if (!string.IsNullOrEmpty(passwordResetProviderId))
 1021            {
 01022                providers = providers.Where(i =>
 01023                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 01024                    .ToArray();
 1025            }
 1026
 01027            if (providers.Length == 0)
 1028            {
 01029                providers = new IPasswordResetProvider[]
 01030                {
 01031                    _defaultPasswordResetProvider
 01032                };
 1033            }
 1034
 01035            return providers;
 1036        }
 1037
 1038        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 1039                string username,
 1040                string password,
 1041                User? user)
 1042        {
 161043            bool success = false;
 161044            IAuthenticationProvider? authenticationProvider = null;
 1045
 481046            foreach (var provider in GetAuthenticationProviders(user))
 1047            {
 161048                var providerAuthResult =
 161049                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 161050                var updatedUsername = providerAuthResult.Username;
 161051                success = providerAuthResult.Success;
 1052
 161053                if (success)
 1054                {
 161055                    authenticationProvider = provider;
 161056                    username = updatedUsername;
 161057                    break;
 1058                }
 01059            }
 1060
 161061            return (authenticationProvider, username, success);
 161062        }
 1063
 1064        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 1065            IAuthenticationProvider provider,
 1066            string username,
 1067            string password,
 1068            User? resolvedUser)
 1069        {
 1070            try
 1071            {
 161072                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 161073                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 161074                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 1075
 161076                if (authenticationResult.Username != username)
 1077                {
 01078                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 01079                    username = authenticationResult.Username;
 1080                }
 1081
 161082                return (username, true);
 1083            }
 01084            catch (AuthenticationException ex)
 1085            {
 01086                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 1087
 01088                return (username, false);
 1089            }
 161090        }
 1091
 1092        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 1093        {
 81094            dbContext.Users.Attach(user);
 81095            dbContext.Entry(user).State = EntityState.Modified;
 81096            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 81097        }
 1098
 1099        /// <inheritdoc/>
 1100        public void Dispose()
 1101        {
 581102            Dispose(true);
 581103            GC.SuppressFinalize(this);
 581104        }
 1105
 1106        /// <summary>
 1107        /// Disposes all members of this class.
 1108        /// </summary>
 1109        /// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
 1110        protected virtual void Dispose(bool disposing)
 1111        {
 581112            if (disposing)
 1113            {
 581114                _userLock.Dispose();
 1115            }
 581116        }
 1117
 1118        internal sealed class LockHelper : IDisposable
 1119        {
 621120            private readonly AsyncKeyedLocker<Guid> _userLock = new();
 1121
 1122            private bool _disposed;
 1123
 21124            public static AsyncLocal<int> IsNestedLock { get; set; } = new();
 1125
 1126            public bool ShouldLock()
 1127            {
 51128                return IsNestedLock.Value == 0;
 1129            }
 1130
 1131            public ValueTask<IDisposable> LockAsync(Guid key)
 1132            {
 541133                ThrowIfDisposed();
 531134                var isNested = LockHelper.IsNestedLock.Value != 0;
 531135                LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value + 1;
 531136                if (isNested)
 1137                {
 11138                    return new ValueTask<IDisposable>(new LockHandle { Parent = null });
 1139                }
 1140
 521141                return AcquireLockAsync(key);
 1142            }
 1143
 1144            private async ValueTask<IDisposable> AcquireLockAsync(Guid key)
 1145            {
 521146                var lockHandle = await _userLock.LockAsync(key, true).ConfigureAwait(false);
 521147                return new LockHandle { Parent = lockHandle };
 521148            }
 1149
 1150            public void Dispose()
 1151            {
 651152                if (_disposed)
 1153                {
 31154                    return;
 1155                }
 1156
 621157                _disposed = true;
 621158                _userLock.Dispose();
 621159            }
 1160
 1161            private void ThrowIfDisposed()
 1162            {
 541163                ObjectDisposedException.ThrowIf(_disposed, this);
 531164            }
 1165
 1166            private sealed class LockHandle : IDisposable
 1167            {
 1168                public required IDisposable? Parent { get; init; }
 1169
 1170                public void Dispose()
 1171                {
 531172                    Parent?.Dispose();
 531173                    LockHelper.IsNestedLock.Value = LockHelper.IsNestedLock.Value - 1;
 1174
 531175                    if (LockHelper.IsNestedLock.Value < 0)
 1176                    {
 01177                        throw new InvalidOperationException("Mismatched locking detected. Threads internal NestedLock is
 1178                    }
 531179                }
 1180            }
 1181        }
 1182    }
 1183}

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()
SyncPermissions(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.ICollection`1<Jellyfin.Database.Implementations.Entities.Permission>)
SyncPreferences(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.ICollection`1<Jellyfin.Database.Implementations.Entities.Preference>)
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()