aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Users
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations/Users')
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs24
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs101
2 files changed, 106 insertions, 19 deletions
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
index 92e2bb4fa7..7c46ef7721 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Users;
@@ -20,6 +22,7 @@ public sealed class DeviceAccessHost : IHostedService
private readonly IUserManager _userManager;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
+ private readonly ILogger<DeviceAccessHost> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceAccessHost"/> class.
@@ -27,11 +30,17 @@ public sealed class DeviceAccessHost : IHostedService
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
/// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
- public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
+ /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
+ public DeviceAccessHost(
+ IUserManager userManager,
+ IDeviceManager deviceManager,
+ ISessionManager sessionManager,
+ ILogger<DeviceAccessHost> logger)
{
_userManager = userManager;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
+ _logger = logger;
}
/// <inheritdoc />
@@ -53,9 +62,18 @@ public sealed class DeviceAccessHost : IHostedService
private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
- if (!user.HasPermission(PermissionKind.EnableAllDevices))
+
+ // This handler is async void, so an escaping exception would terminate the process.
+ try
+ {
+ if (!user.HasPermission(PermissionKind.EnableAllDevices))
+ {
+ await UpdateDeviceAccess(user).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
{
- await UpdateDeviceAccess(user).ConfigureAwait(false);
+ _logger.LogError(ex, "Error updating device access for user {UserId}", user.Id);
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 583f29f94f..b15f6b98b2 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -225,17 +225,8 @@ namespace Jellyfin.Server.Implementations.Users
?? throw new ResourceNotFoundException(nameof(user.Id));
dbContext.Entry(dbUser).CurrentValues.SetValues(user);
- dbUser.Permissions.Clear();
- foreach (var permission in user.Permissions)
- {
- dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value));
- }
-
- dbUser.Preferences.Clear();
- foreach (var preference in user.Preferences)
- {
- dbUser.Preferences.Add(new Preference(preference.Kind, preference.Value));
- }
+ SyncPermissions(dbUser, user.Permissions);
+ SyncPreferences(dbUser, user.Preferences);
dbUser.AccessSchedules.Clear();
foreach (var accessSchedule in user.AccessSchedules)
@@ -269,6 +260,60 @@ namespace Jellyfin.Server.Implementations.Users
}
}
+ private static void SyncPermissions(User dbUser, ICollection<Permission> source)
+ {
+ var incoming = new Dictionary<PermissionKind, bool>();
+ foreach (var permission in source)
+ {
+ incoming[permission.Kind] = permission.Value;
+ }
+
+ foreach (var existing in dbUser.Permissions)
+ {
+ if (incoming.Remove(existing.Kind, out var value))
+ {
+ // EF only marks the row modified if the value actually differs, so an update that
+ // touches nothing but the user row - a session activity stamp - writes no children.
+ existing.Value = value;
+ }
+ else
+ {
+ dbUser.Permissions.Remove(existing);
+ }
+ }
+
+ foreach (var (kind, value) in incoming)
+ {
+ dbUser.Permissions.Add(new Permission(kind, value));
+ }
+ }
+
+ private static void SyncPreferences(User dbUser, ICollection<Preference> source)
+ {
+ var incoming = new Dictionary<PreferenceKind, string>();
+ foreach (var preference in source)
+ {
+ incoming[preference.Kind] = preference.Value;
+ }
+
+ foreach (var existing in dbUser.Preferences)
+ {
+ if (incoming.Remove(existing.Kind, out var value))
+ {
+ existing.Value = value;
+ }
+ else
+ {
+ dbUser.Preferences.Remove(existing);
+ }
+ }
+
+ foreach (var (kind, value) in incoming)
+ {
+ dbUser.Preferences.Add(new Preference(kind, value));
+ }
+ }
+
internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
{
// TODO: Remove after user item data is migrated.
@@ -616,6 +661,12 @@ namespace Jellyfin.Server.Implementations.Users
.SetProperty(f => f.LastActivityDate, date)
.SetProperty(f => f.LastLoginDate, date))
.ConfigureAwait(false);
+
+ // ExecuteUpdateAsync bypasses the change tracker, so keep the
+ // returned entity in sync. Otherwise SessionManager.LogSessionActivity
+ // saves this (stale) entity in full and reverts LastLoginDate.
+ user.LastActivityDate = date;
+ user.LastLoginDate = date;
}
await dbContext.Users
@@ -796,14 +847,16 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
{
+ User user;
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var user = UserQuery(dbContext)
+ user = await UserQuery(dbContext)
.AsTracking()
- .FirstOrDefault(u => u.Id.Equals(userId))
+ .FirstOrDefaultAsync(u => u.Id.Equals(userId))
+ .ConfigureAwait(false)
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@@ -868,6 +921,10 @@ namespace Jellyfin.Server.Implementations.Users
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
+
+ var eventArgs = new UserUpdatedEventArgs(user);
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+ OnUserUpdated?.Invoke(this, eventArgs);
}
/// <inheritdoc/>
@@ -883,8 +940,20 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- dbContext.Remove(user.ProfileImage);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ // Remove the tracked profile image loaded from the database instead of the
+ // detached instance on the passed in user. That instance can carry a stale,
+ // never-persisted (temporary) key, which makes EF Core throw when it is marked
+ // for deletion, leaving the profile image impossible to clear or replace.
+ var dbUser = await UserQuery(dbContext)
+ .AsTracking()
+ .FirstOrDefaultAsync(u => u.Id == user.Id)
+ .ConfigureAwait(false);
+ if (dbUser?.ProfileImage is not null)
+ {
+ dbContext.Remove(dbUser.ProfileImage);
+ dbUser.ProfileImage = null;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
user.ProfileImage = null;
@@ -893,7 +962,7 @@ namespace Jellyfin.Server.Implementations.Users
internal static void ThrowIfInvalidUsername(string name)
{
- if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
+ if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name) && !string.Equals(name, ".", StringComparison.Ordinal) && !string.Equals(name, "..", StringComparison.Ordinal))
{
return;
}