mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-15 10:43:30 +01:00
Merge branch 'master' into bug/authorization-header-issue
This commit is contained in:
@@ -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)
|
||||
|
||||
243
Jellyfin.Server.Implementations/Devices/DeviceManager.cs
Normal file
243
Jellyfin.Server.Implementations/Devices/DeviceManager.cs
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -196,10 +203,30 @@ namespace Jellyfin.Server.Implementations
|
||||
|
||||
// Indexes
|
||||
|
||||
modelBuilder.Entity<ApiKey>()
|
||||
.HasIndex(entity => entity.AccessToken)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasIndex(entity => entity.Username)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Device>()
|
||||
.HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
|
||||
|
||||
modelBuilder.Entity<Device>()
|
||||
.HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
|
||||
|
||||
modelBuilder.Entity<Device>()
|
||||
.HasIndex(entity => new { entity.UserId, entity.DeviceId });
|
||||
|
||||
modelBuilder.Entity<Device>()
|
||||
.HasIndex(entity => entity.DeviceId);
|
||||
|
||||
modelBuilder.Entity<DeviceOptions>()
|
||||
.HasIndex(entity => entity.DeviceId)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<DisplayPreferences>()
|
||||
.HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
|
||||
.IsUnique();
|
||||
|
||||
653
Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
generated
Normal file
653
Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
generated
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
309
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
Normal file
309
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
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].ToString();
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Add last value
|
||||
if (start < i)
|
||||
{
|
||||
result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,15 +163,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user