aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs243
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs81
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs14
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs653
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs128
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs121
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs28
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs25
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs24
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs21
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs56
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthenticationManager.cs73
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs308
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs22
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs17
20 files changed, 1778 insertions, 103 deletions
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index a3a2a8baf..ba2c8b54f 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Activity
return new QueryResult<ActivityLogEntry>
{
Items = await entries
- .Skip(query.StartIndex ?? 0)
+ .Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.AsAsyncEnumerable()
.Select(ConvertToOldModel)
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
new file mode 100644
index 000000000..0655c9813
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Session;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Devices
+{
+ /// <summary>
+ /// Manages the creation, updating, and retrieval of devices.
+ /// </summary>
+ public class DeviceManager : IDeviceManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+ private readonly IUserManager _userManager;
+ private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="userManager">The user manager.</param>
+ public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
+ {
+ _dbProvider = dbProvider;
+ _userManager = userManager;
+ }
+
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>>? DeviceOptionsUpdated;
+
+ /// <inheritdoc />
+ public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
+ {
+ _capabilitiesMap[deviceId] = capabilities;
+ }
+
+ /// <inheritdoc />
+ public async Task UpdateDeviceOptions(string deviceId, string deviceName)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+ if (deviceOptions == null)
+ {
+ deviceOptions = new DeviceOptions(deviceId);
+ dbContext.DeviceOptions.Add(deviceOptions);
+ }
+
+ deviceOptions.CustomName = deviceName;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+
+ DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
+ }
+
+ /// <inheritdoc />
+ public async Task<Device> CreateDevice(Device device)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ dbContext.Devices.Add(device);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ return device;
+ }
+
+ /// <inheritdoc />
+ public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var deviceOptions = await dbContext.DeviceOptions
+ .AsQueryable()
+ .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
+ .ConfigureAwait(false);
+
+ return deviceOptions ?? new DeviceOptions(deviceId);
+ }
+
+ /// <inheritdoc />
+ public ClientCapabilities GetCapabilities(string deviceId)
+ {
+ return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result)
+ ? result
+ : new ClientCapabilities();
+ }
+
+ /// <inheritdoc />
+ public async Task<DeviceInfo?> GetDevice(string id)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var device = await dbContext.Devices
+ .AsQueryable()
+ .Where(d => d.DeviceId == id)
+ .OrderByDescending(d => d.DateLastActivity)
+ .Include(d => d.User)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+
+ var deviceInfo = device == null ? null : ToDeviceInfo(device);
+
+ return deviceInfo;
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ var devices = dbContext.Devices.AsQueryable();
+
+ if (query.UserId.HasValue)
+ {
+ devices = devices.Where(device => device.UserId == query.UserId.Value);
+ }
+
+ if (query.DeviceId != null)
+ {
+ devices = devices.Where(device => device.DeviceId == query.DeviceId);
+ }
+
+ if (query.AccessToken != null)
+ {
+ devices = devices.Where(device => device.AccessToken == query.AccessToken);
+ }
+
+ var count = await devices.CountAsync().ConfigureAwait(false);
+
+ if (query.Skip.HasValue)
+ {
+ devices = devices.Skip(query.Skip.Value);
+ }
+
+ if (query.Limit.HasValue)
+ {
+ devices = devices.Take(query.Limit.Value);
+ }
+
+ return new QueryResult<Device>
+ {
+ Items = await devices.ToListAsync().ConfigureAwait(false),
+ StartIndex = query.Skip ?? 0,
+ TotalRecordCount = count
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
+ {
+ var devices = await GetDevices(query).ConfigureAwait(false);
+
+ return new QueryResult<DeviceInfo>
+ {
+ Items = devices.Items.Select(device => ToDeviceInfo(device)).ToList(),
+ StartIndex = devices.StartIndex,
+ TotalRecordCount = devices.TotalRecordCount
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var sessions = dbContext.Devices
+ .Include(d => d.User)
+ .AsQueryable()
+ .OrderBy(d => d.DeviceId)
+ .ThenByDescending(d => d.DateLastActivity)
+ .AsAsyncEnumerable();
+
+ if (supportsSync.HasValue)
+ {
+ sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+ }
+
+ if (userId.HasValue)
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
+ }
+
+ var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
+
+ return new QueryResult<DeviceInfo>(array);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteDevice(Device device)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ dbContext.Devices.Remove(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public bool CanAccessDevice(User user, string deviceId)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (string.IsNullOrEmpty(deviceId))
+ {
+ throw new ArgumentNullException(nameof(deviceId));
+ }
+
+ if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+ {
+ return true;
+ }
+
+ return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)
+ || !GetCapabilities(deviceId).SupportsPersistentIdentifier;
+ }
+
+ private DeviceInfo ToDeviceInfo(Device authInfo)
+ {
+ var caps = GetCapabilities(authInfo.DeviceId);
+
+ return new DeviceInfo
+ {
+ AppName = authInfo.AppName,
+ AppVersion = authInfo.AppVersion,
+ Id = authInfo.DeviceId,
+ LastUserId = authInfo.UserId,
+ LastUserName = authInfo.User.Username,
+ Name = authInfo.DeviceName,
+ DateLastActivity = authInfo.DateLastActivity,
+ IconUrl = caps.IconUrl
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index db648472d..dc4f53913 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
@@ -29,6 +30,12 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+ public virtual DbSet<ApiKey> ApiKeys { get; set; }
+
+ public virtual DbSet<Device> Devices { get; set; }
+
+ public virtual DbSet<DeviceOptions> DeviceOptions { get; set; }
+
public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
@@ -146,80 +153,10 @@ namespace Jellyfin.Server.Implementations
{
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
base.OnModelCreating(modelBuilder);
-
modelBuilder.HasDefaultSchema("jellyfin");
- // Collations
-
- modelBuilder.Entity<User>()
- .Property(user => user.Username)
- .UseCollation("NOCASE");
-
- // Delete behavior
-
- modelBuilder.Entity<User>()
- .HasOne(u => u.ProfileImage)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Permissions)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Preferences)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.AccessSchedules)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.DisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.ItemDisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasMany(d => d.HomeSections)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- // Indexes
-
- modelBuilder.Entity<User>()
- .HasIndex(entity => entity.Username)
- .IsUnique();
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
- .IsUnique();
-
- modelBuilder.Entity<CustomItemDisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
- .IsUnique();
-
- // Used to get a user's permissions or a specific permission for a user.
- // Also prevents multiple values being created for a user.
- // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
- modelBuilder.Entity<Permission>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
-
- modelBuilder.Entity<Preference>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
+ // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly);
}
}
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index 486be6053..c2c5198d1 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -1,8 +1,10 @@
using System;
using System.IO;
+using System.Linq;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations
{
@@ -13,19 +15,27 @@ namespace Jellyfin.Server.Implementations
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationPaths _appPaths;
+ private readonly ILogger<JellyfinDbProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
/// <param name="appPaths">The application paths.</param>
- public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
+ /// <param name="logger">The logger.</param>
+ public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger)
{
_serviceProvider = serviceProvider;
_appPaths = appPaths;
+ _logger = logger;
using var jellyfinDb = CreateContext();
- jellyfinDb.Database.Migrate();
+ if (jellyfinDb.Database.GetPendingMigrations().Any())
+ {
+ _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+ jellyfinDb.Database.Migrate();
+ _logger.LogInformation("EFCore migrations applied successfully");
+ }
}
/// <summary>
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
new file mode 100644
index 000000000..7e9566e2e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
@@ -0,0 +1,653 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20210814002109_AddDevices")]
+ partial class AddDevices
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "5.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("EasyPassword")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs
new file mode 100644
index 000000000..ac062317a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs
@@ -0,0 +1,128 @@
+#pragma warning disable CS1591, SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddDevices : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "ApiKeys",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateLastActivity = table.Column<DateTime>(type: "TEXT", nullable: false),
+ Name = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ AccessToken = table.Column<string>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ApiKeys", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "DeviceOptions",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ DeviceId = table.Column<string>(type: "TEXT", nullable: false),
+ CustomName = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_DeviceOptions", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Devices",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(type: "TEXT", nullable: false),
+ AccessToken = table.Column<string>(type: "TEXT", nullable: false),
+ AppName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ AppVersion = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
+ DeviceName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ DeviceId = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
+ IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
+ DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateLastActivity = table.Column<DateTime>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Devices", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Devices_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ApiKeys_AccessToken",
+ schema: "jellyfin",
+ table: "ApiKeys",
+ column: "AccessToken",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DeviceOptions_DeviceId",
+ schema: "jellyfin",
+ table: "DeviceOptions",
+ column: "DeviceId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_AccessToken_DateLastActivity",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "AccessToken", "DateLastActivity" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_DeviceId",
+ schema: "jellyfin",
+ table: "Devices",
+ column: "DeviceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_DeviceId_DateLastActivity",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "DeviceId", "DateLastActivity" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_UserId_DeviceId",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "UserId", "DeviceId" });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "ApiKeys",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "DeviceOptions",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "Devices",
+ schema: "jellyfin");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 286eb7468..fcc360e26 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "5.0.3");
+ .HasAnnotation("ProductVersion", "5.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -332,6 +332,114 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("Preferences");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
@@ -505,6 +613,17 @@ namespace Jellyfin.Server.Implementations.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
new file mode 100644
index 000000000..3f19b6986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the ApiKey entity.
+ /// </summary>
+ public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ApiKey> builder)
+ {
+ builder
+ .HasIndex(entity => entity.AccessToken)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..779aec986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the CustomItemDisplayPreferences entity.
+ /// </summary>
+ public class CustomItemDisplayPreferencesConfiguration : IEntityTypeConfiguration<CustomItemDisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<CustomItemDisplayPreferences> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
new file mode 100644
index 000000000..a750b65c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
@@ -0,0 +1,28 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Device entity.
+ /// </summary>
+ public class DeviceConfiguration : IEntityTypeConfiguration<Device>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Device> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.DeviceId });
+
+ builder
+ .HasIndex(entity => entity.DeviceId);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
new file mode 100644
index 000000000..038afd752
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DeviceOptions entity.
+ /// </summary>
+ public class DeviceOptionsConfiguration : IEntityTypeConfiguration<DeviceOptions>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DeviceOptions> builder)
+ {
+ builder
+ .HasIndex(entity => entity.DeviceId)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..9b437861b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DisplayPreferencesConfiguration entity.
+ /// </summary>
+ public class DisplayPreferencesConfiguration : IEntityTypeConfiguration<DisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DisplayPreferences> builder)
+ {
+ builder
+ .HasMany(d => d.HomeSections)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
new file mode 100644
index 000000000..240e284c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
@@ -0,0 +1,24 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Permission> builder)
+ {
+ // Used to get a user's permissions or a specific permission for a user.
+ // Also prevents multiple values being created for a user.
+ // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
new file mode 100644
index 000000000..49c869c6a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PreferenceConfiguration : IEntityTypeConfiguration<Preference>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Preference> builder)
+ {
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
new file mode 100644
index 000000000..a369cf656
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
@@ -0,0 +1,56 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the User entity.
+ /// </summary>
+ public class UserConfiguration : IEntityTypeConfiguration<User>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<User> builder)
+ {
+ builder
+ .Property(user => user.Username)
+ .UseCollation("NOCASE");
+
+ builder
+ .HasOne(u => u.ProfileImage)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Permissions)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Preferences)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.AccessSchedules)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.DisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.ItemDisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => entity.Username)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
new file mode 100644
index 000000000..b79e46469
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities.Security;
+using MediaBrowser.Controller.Security;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Security
+{
+ /// <inheritdoc />
+ public class AuthenticationManager : IAuthenticationManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database provider.</param>
+ public AuthenticationManager(JellyfinDbProvider dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task CreateApiKey(string name)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ dbContext.ApiKeys.Add(new ApiKey(name));
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ return await dbContext.ApiKeys
+ .AsAsyncEnumerable()
+ .Select(key => new AuthenticationInfo
+ {
+ AppName = key.Name,
+ AccessToken = key.AccessToken,
+ DateCreated = key.DateCreated,
+ DeviceId = string.Empty,
+ DeviceName = string.Empty,
+ AppVersion = string.Empty
+ }).ToListAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteApiKey(string accessToken)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ var key = await dbContext.ApiKeys
+ .AsQueryable()
+ .Where(apiKey => apiKey.AccessToken == accessToken)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+
+ if (key == null)
+ {
+ return;
+ }
+
+ dbContext.Remove(key);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
new file mode 100644
index 000000000..244abf469
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -0,0 +1,308 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Server.Implementations.Security
+{
+ public class AuthorizationContext : IAuthorizationContext
+ {
+ private readonly JellyfinDbProvider _jellyfinDbProvider;
+ private readonly IUserManager _userManager;
+
+ public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
+ {
+ _jellyfinDbProvider = jellyfinDb;
+ _userManager = userManager;
+ }
+
+ public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
+ {
+ if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
+ {
+ return Task.FromResult((AuthorizationInfo)cached!); // Cache should never contain null
+ }
+
+ return GetAuthorization(requestContext);
+ }
+
+ public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
+ {
+ var auth = GetAuthorizationDictionary(requestContext);
+ var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
+ return authInfo;
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
+ {
+ var auth = GetAuthorizationDictionary(httpReq);
+ var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
+
+ httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+ return authInfo;
+ }
+
+ private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
+ IReadOnlyDictionary<string, string>? auth,
+ IHeaderDictionary headers,
+ IQueryCollection queryString)
+ {
+ string? deviceId = null;
+ string? deviceName = null;
+ string? client = null;
+ string? version = null;
+ string? token = null;
+
+ if (auth != null)
+ {
+ auth.TryGetValue("DeviceId", out deviceId);
+ auth.TryGetValue("Device", out deviceName);
+ auth.TryGetValue("Client", out client);
+ auth.TryGetValue("Version", out version);
+ auth.TryGetValue("Token", out token);
+ }
+
+#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
+ if (string.IsNullOrEmpty(token))
+ {
+ token = headers["X-Emby-Token"];
+ }
+
+ if (string.IsNullOrEmpty(token))
+ {
+ token = headers["X-MediaBrowser-Token"];
+ }
+
+ if (string.IsNullOrEmpty(token))
+ {
+ token = queryString["ApiKey"];
+ }
+
+ // TODO deprecate this query parameter.
+ if (string.IsNullOrEmpty(token))
+ {
+ token = queryString["api_key"];
+ }
+
+ var authInfo = new AuthorizationInfo
+ {
+ Client = client,
+ Device = deviceName,
+ DeviceId = deviceId,
+ Version = version,
+ Token = token,
+ IsAuthenticated = false,
+ HasToken = false
+ };
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ // Request doesn't contain a token.
+ return authInfo;
+ }
+#pragma warning restore CA1508
+
+ authInfo.HasToken = true;
+ await using var dbContext = _jellyfinDbProvider.CreateContext();
+ var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
+
+ if (device != null)
+ {
+ authInfo.IsAuthenticated = true;
+ var updateToken = false;
+
+ // TODO: Remove these checks for IsNullOrWhiteSpace
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
+ {
+ authInfo.Client = device.AppName;
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = device.DeviceId;
+ }
+
+ // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+ var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
+
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
+ {
+ authInfo.Device = device.DeviceName;
+ }
+ else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
+ {
+ updateToken = true;
+ device.DeviceName = authInfo.Device;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = device.AppVersion;
+ }
+ else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
+ {
+ updateToken = true;
+ device.AppVersion = authInfo.Version;
+ }
+ }
+
+ if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
+ {
+ device.DateLastActivity = DateTime.UtcNow;
+ updateToken = true;
+ }
+
+ authInfo.User = _userManager.GetUserById(device.UserId);
+
+ if (updateToken)
+ {
+ dbContext.Devices.Update(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
+ if (key != null)
+ {
+ authInfo.IsAuthenticated = true;
+ authInfo.Client = key.Name;
+ authInfo.Token = key.AccessToken;
+ authInfo.DeviceId = string.Empty;
+ authInfo.Device = string.Empty;
+ authInfo.Version = string.Empty;
+ authInfo.IsApiKey = true;
+ }
+ }
+
+ return authInfo;
+ }
+
+ /// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
+ {
+ var auth = httpReq.Request.Headers["X-Emby-Authorization"];
+
+ if (string.IsNullOrEmpty(auth))
+ {
+ auth = httpReq.Request.Headers[HeaderNames.Authorization];
+ }
+
+ return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
+ }
+
+ /// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
+ {
+ var auth = httpReq.Headers["X-Emby-Authorization"];
+
+ if (string.IsNullOrEmpty(auth))
+ {
+ auth = httpReq.Headers[HeaderNames.Authorization];
+ }
+
+ return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
+ {
+ var firstSpace = authorizationHeader.IndexOf(' ');
+
+ // There should be at least two parts
+ if (firstSpace == -1)
+ {
+ return null;
+ }
+
+ var name = authorizationHeader[..firstSpace];
+
+ if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
+ && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // Remove up until the first space
+ authorizationHeader = authorizationHeader[(firstSpace + 1)..];
+ return GetParts(authorizationHeader);
+ }
+
+ /// <summary>
+ /// Get the authorization header components.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
+ {
+ var result = new Dictionary<string, string>();
+ var escaped = false;
+ int start = 0;
+ string key = string.Empty;
+
+ int i;
+ for (i = 0; i < authorizationHeader.Length; i++)
+ {
+ var token = authorizationHeader[i];
+ if (token == '"' || token == ',')
+ {
+ // Applying a XOR logic to evaluate whether it is opening or closing a value
+ escaped = (!escaped) == (token == '"');
+ if (token == ',' && !escaped)
+ {
+ // Meeting a comma after a closing escape char means the value is complete
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ key = string.Empty;
+ }
+
+ start = i + 1;
+ }
+ }
+ else if (!escaped && token == '=')
+ {
+ key = authorizationHeader[start.. i].Trim().ToString();
+ start = i + 1;
+ }
+ }
+
+ // Add last value
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index c99c5e4ef..6e98ad863 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
- await using (var str = File.OpenRead(resetFile))
+ await using (var str = AsyncFile.OpenRead(resetFile))
{
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -110,7 +111,7 @@ namespace Jellyfin.Server.Implementations.Users
UserName = user.Username
};
- await using (FileStream fileStream = File.OpenWrite(filePath))
+ await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
await fileStream.FlushAsync().ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
index dbba80c21..a471ea1d5 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
@@ -4,10 +4,10 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
namespace Jellyfin.Server.Implementations.Users
@@ -15,14 +15,12 @@ namespace Jellyfin.Server.Implementations.Users
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
- private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
- public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+ public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
{
_userManager = userManager;
- _authRepo = authRepo;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
}
@@ -38,27 +36,27 @@ namespace Jellyfin.Server.Implementations.Users
{
}
- private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+ private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
{
- UpdateDeviceAccess(user);
+ await UpdateDeviceAccess(user).ConfigureAwait(false);
}
}
- private void UpdateDeviceAccess(User user)
+ private async Task UpdateDeviceAccess(User user)
{
- var existing = _authRepo.Get(new AuthenticationInfoQuery
+ var existing = (await _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
- }).Items;
+ }).ConfigureAwait(false)).Items;
- foreach (var authInfo in existing)
+ foreach (var device in existing)
{
- if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+ if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
{
- _sessionManager.Logout(authInfo);
+ await _sessionManager.Logout(device).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 27d4f40d3..02377bfd7 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -164,15 +164,6 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void UpdateUser(User user)
- {
- using var dbContext = _dbProvider.CreateContext();
- dbContext.Users.Update(user);
- _users[user.Id] = user;
- dbContext.SaveChanges();
- }
-
- /// <inheritdoc/>
public async Task UpdateUserAsync(User user)
{
await using var dbContext = _dbProvider.CreateContext();
@@ -271,9 +262,9 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ResetEasyPassword(User user)
+ public Task ResetEasyPassword(User user)
{
- ChangeEasyPassword(user, string.Empty, null);
+ return ChangeEasyPassword(user, string.Empty, null);
}
/// <inheritdoc/>
@@ -291,7 +282,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
+ public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
if (newPassword != null)
{
@@ -304,7 +295,7 @@ namespace Jellyfin.Server.Implementations.Users
}
user.EasyPassword = newPasswordSha1;
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
_eventManager.Publish(new UserPasswordChangedEventArgs(user));
}