Migrate User DB to EF Core

pull/3423/head
Patrick Barron 4 years ago
parent aca7e221d8
commit 3eeb6576d8

@ -562,7 +562,6 @@ namespace Emby.Server.Implementations
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
serviceCollection.AddSingleton<IUserManager, UserManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation

@ -0,0 +1,225 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
/// <summary>
/// Class SQLiteDisplayPreferencesRepository.
/// </summary>
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
{
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonOptions;
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
: base(logger)
{
_fileSystem = fileSystem;
_jsonOptions = JsonDefaults.GetOptions();
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
}
/// <summary>
/// Gets the name of the repository.
/// </summary>
/// <value>The name.</value>
public string Name => "SQLite";
public void Initialize()
{
try
{
InitializeInternal();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
_fileSystem.DeleteFile(DbFilePath);
InitializeInternal();
}
}
/// <summary>
/// Opens the connection to the database
/// </summary>
/// <returns>Task.</returns>
private void InitializeInternal()
{
string[] queries =
{
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
};
using (var connection = GetConnection())
{
connection.RunQueries(queries);
}
}
/// <summary>
/// Save the display preferences associated with an item in the repo
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
if (string.IsNullOrEmpty(displayPreferences.Id))
{
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
TransactionMode);
}
}
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
{
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
{
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}
/// <summary>
/// Save all display preferences associated with a user in the repo
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
foreach (var displayPreference in displayPreferences)
{
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
}
},
TransactionMode);
}
}
/// <summary>
/// Gets the display preferences.
/// </summary>
/// <param name="displayPreferencesId">The display preferences id.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
{
if (string.IsNullOrEmpty(displayPreferencesId))
{
throw new ArgumentNullException(nameof(displayPreferencesId));
}
var guidId = displayPreferencesId.GetMD5();
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
{
statement.TryBind("@id", guidId.ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
foreach (var row in statement.ExecuteQuery())
{
return Get(row);
}
}
}
return new DisplayPreferences
{
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
};
}
/// <summary>
/// Gets all display preferences for the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
{
var list = new List<DisplayPreferences>();
using (var connection = GetConnection(true))
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
{
statement.TryBind("@userId", userId.ToByteArray());
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
}
return list;
}
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
}
}

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@ -14,7 +15,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@ -27,6 +27,7 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Emby.Server.Implementations.Session
{
@ -254,7 +255,7 @@ namespace Emby.Server.Implementations.Session
string deviceId,
string deviceName,
string remoteEndPoint,
Jellyfin.Data.Entities.User user)
User user)
{
CheckDisposed();
@ -438,7 +439,7 @@ namespace Emby.Server.Implementations.Session
string deviceId,
string deviceName,
string remoteEndPoint,
Jellyfin.Data.Entities.User user)
User user)
{
CheckDisposed();
@ -457,7 +458,7 @@ namespace Emby.Server.Implementations.Session
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = user?.Username;
sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user);
sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
sessionInfo.Client = appName;
@ -483,7 +484,7 @@ namespace Emby.Server.Implementations.Session
string deviceId,
string deviceName,
string remoteEndPoint,
Jellyfin.Data.Entities.User user)
User user)
{
var sessionInfo = new SessionInfo(this, _logger)
{
@ -497,7 +498,7 @@ namespace Emby.Server.Implementations.Session
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = username;
sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user);
sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
if (string.IsNullOrEmpty(deviceName))
@ -520,9 +521,9 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
private List<Jellyfin.Data.Entities.User> GetUsers(SessionInfo session)
private List<User> GetUsers(SessionInfo session)
{
var users = new List<Jellyfin.Data.Entities.User>();
var users = new List<User>();
if (session.UserId != Guid.Empty)
{
@ -680,7 +681,7 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="user">The user object.</param>
/// <param name="item">The item.</param>
private void OnPlaybackStart(Jellyfin.Data.Entities.User user, BaseItem item)
private void OnPlaybackStart(User user, BaseItem item)
{
var data = _userDataManager.GetUserData(user, item);
@ -763,7 +764,7 @@ namespace Emby.Server.Implementations.Session
StartIdleCheckTimer();
}
private void OnPlaybackProgress(Jellyfin.Data.Entities.User user, BaseItem item, PlaybackProgressInfo info)
private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
{
var data = _userDataManager.GetUserData(user, item);
@ -789,7 +790,7 @@ namespace Emby.Server.Implementations.Session
}
}
private static bool UpdatePlaybackSettings(Jellyfin.Data.Entities.User user, PlaybackProgressInfo info, UserItemData data)
private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
{
var changed = false;
@ -949,7 +950,7 @@ namespace Emby.Server.Implementations.Session
_logger);
}
private bool OnPlaybackStopped(Jellyfin.Data.Entities.User user, BaseItem item, long? positionTicks, bool playbackFailed)
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
bool playedToCompletion = false;
@ -1163,7 +1164,7 @@ namespace Emby.Server.Implementations.Session
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, Jellyfin.Data.Entities.User user)
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
@ -1216,7 +1217,7 @@ namespace Emby.Server.Implementations.Session
return new[] { item };
}
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, Jellyfin.Data.Entities.User user)
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
@ -1399,7 +1400,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
Jellyfin.Data.Entities.User user = null;
User user = null;
if (request.UserId != Guid.Empty)
{
user = _userManager.GetUserById(request.UserId);
@ -1455,7 +1456,7 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
private string GetAuthorizationToken(Jellyfin.Data.Entities.User user, string deviceId, string app, string appVersion, string deviceName)
private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
var existing = _authRepo.Get(
new AuthenticationInfoQuery
@ -1701,7 +1702,7 @@ namespace Emby.Server.Implementations.Session
return info;
}
private string GetImageCacheTag(Jellyfin.Data.Entities.User user)
private string GetImageCacheTag(User user)
{
try
{

@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
@ -20,8 +22,9 @@ namespace Jellyfin.Data.Entities
/// <param name="dayOfWeek">The day of the week.</param>
/// <param name="startHour">The start hour.</param>
/// <param name="endHour">The end hour.</param>
public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour)
public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
{
UserId = userId;
DayOfWeek = dayOfWeek;
StartHour = startHour;
EndHour = endHour;
@ -34,15 +37,20 @@ namespace Jellyfin.Data.Entities
/// <param name="startHour">The start hour.</param>
/// <param name="endHour">The end hour.</param>
/// <returns>The newly created instance.</returns>
public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour)
public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
{
return new AccessSchedule(dayOfWeek, startHour, endHour);
return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
}
[JsonIgnore]
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
public int Id { get; set; }
[Required]
[ForeignKey("Id")]
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the day of week.

@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities
{
public partial class Group
public partial class Group : IHasPermissions, ISavingChanges
{
partial void Init();
@ -14,35 +14,29 @@ namespace Jellyfin.Data.Entities
/// </summary>
protected Group()
{
GroupPermissions = new HashSet<Permission>();
Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// </summary>
public static Group CreateGroupUnsafe()
{
return new Group();
}
/// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="name"></param>
/// <param name="_user0"></param>
public Group(string name, User _user0)
/// <param name="user"></param>
public Group(string name, User user)
{
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
this.Name = name;
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Groups.Add(this);
this.Name = name;
user.Groups.Add(this);
this.GroupPermissions = new HashSet<Permission>();
this.Permissions = new HashSet<Permission>();
this.ProviderMappings = new HashSet<ProviderMapping>();
this.Preferences = new HashSet<Preference>();
@ -54,9 +48,9 @@ namespace Jellyfin.Data.Entities
/// </summary>
/// <param name="name"></param>
/// <param name="_user0"></param>
public static Group Create(string name, User _user0)
public static Group Create(string name, User user)
{
return new Group(name, _user0);
return new Group(name, user);
}
/*************************************************************************
@ -68,8 +62,7 @@ namespace Jellyfin.Data.Entities
/// </summary>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
public Guid Id { get; protected set; }
/// <summary>
/// Required, Max length = 255
@ -96,13 +89,13 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
[ForeignKey("Permission_GroupPermissions_Id")]
public virtual ICollection<Permission> GroupPermissions { get; protected set; }
public ICollection<Permission> Permissions { get; protected set; }
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
public ICollection<ProviderMapping> ProviderMappings { get; protected set; }
[ForeignKey("Preference_Preferences_Id")]
public virtual ICollection<Preference> Preferences { get; protected set; }
public ICollection<Preference> Preferences { get; protected set; }
}
}

@ -3,10 +3,11 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.CompilerServices;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class Permission
public partial class Permission : ISavingChanges
{
partial void Init();
@ -18,33 +19,16 @@ namespace Jellyfin.Data.Entities
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// </summary>
public static Permission CreatePermissionUnsafe()
{
return new Permission();
}
/// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
/// <param name="holderId"></param>
public Permission(PermissionKind kind, bool value)
{
this.Kind = kind;
this.Value = value;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Permissions.Add(this);
if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
_group1.GroupPermissions.Add(this);
Kind = kind;
Value = value;
Init();
}
@ -54,11 +38,10 @@ namespace Jellyfin.Data.Entities
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
/// <param name="holderId"></param>
public static Permission Create(PermissionKind kind, bool value)
{
return new Permission(kind, value, _user0, _group1);
return new Permission(kind, value);
}
/*************************************************************************
@ -76,31 +59,32 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Backing field for Kind
/// </summary>
protected Enums.PermissionKind _Kind;
protected PermissionKind _Kind;
/// <summary>
/// When provided in a partial class, allows value of Kind to be changed before setting.
/// </summary>
partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue);
partial void SetKind(PermissionKind oldValue, ref PermissionKind newValue);
/// <summary>
/// When provided in a partial class, allows value of Kind to be changed before returning.
/// </summary>
partial void GetKind(ref Enums.PermissionKind result);
partial void GetKind(ref PermissionKind result);
/// <summary>
/// Required
/// </summary>
[Required]
public Enums.PermissionKind Kind
public PermissionKind Kind
{
get
{
Enums.PermissionKind value = _Kind;
PermissionKind value = _Kind;
GetKind(ref value);
return (_Kind = value);
return _Kind = value;
}
set
{
Enums.PermissionKind oldValue = _Kind;
PermissionKind oldValue = _Kind;
SetKind(oldValue, ref value);
if (oldValue != value)
{
@ -117,7 +101,7 @@ namespace Jellyfin.Data.Entities
public bool Value { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Required, ConcurrencyToken.
/// </summary>
[ConcurrencyCheck]
[Required]
@ -138,7 +122,6 @@ namespace Jellyfin.Data.Entities
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

@ -1,63 +1,33 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class Preference
/// <summary>
/// An entity representing a preference attached to a user or group.
/// </summary>
public class Preference : ISavingChanges
{
partial void Init();
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected Preference()
{
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// Initializes a new instance of the <see cref="Preference"/> class.
/// Public constructor with required data.
/// </summary>
public static Preference CreatePreferenceUnsafe()
/// <param name="kind">The preference kind.</param>
/// <param name="value">The value.</param>
public Preference(PreferenceKind kind, string value)
{
return new Preference();
Kind = kind;
Value = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
{
this.Kind = kind;
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
this.Value = value;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Preferences.Add(this);
if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
_group1.Preferences.Add(this);
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// Initializes a new instance of the <see cref="Preference"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
protected Preference()
{
return new Preference(kind, value, _user0, _group1);
}
/*************************************************************************
@ -76,7 +46,7 @@ namespace Jellyfin.Data.Entities
/// Required
/// </summary>
[Required]
public Enums.PreferenceKind Kind { get; set; }
public PreferenceKind Kind { get; set; }
/// <summary>
/// Required, Max length = 65535
@ -87,21 +57,28 @@ namespace Jellyfin.Data.Entities
public string Value { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Required, ConcurrencyToken.
/// </summary>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="kind">The preference kind.</param>
/// <param name="value">The value.</param>
/// <returns>The new instance.</returns>
public static Preference Create(PreferenceKind kind, string value)
{
return new Preference(kind, value);
}
/// <inheritdoc/>
public void OnSavingChanges()
{
RowVersion++;
}
/*************************************************************************
* Navigation properties
*************************************************************************/
}
}

@ -9,45 +9,23 @@ using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class User
/// <summary>
/// An entity representing a user.
/// </summary>
public partial class User : IHasPermissions, ISavingChanges
{
/// <summary>
/// The values being delimited here are Guids, so commas work as they do not appear in Guids.
/// </summary>
private const char Delimiter = ',';
partial void Init();
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected User()
{
Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
AccessSchedules = new HashSet<AccessSchedule>();
Init();
}
/// <summary>
/// Public constructor with required data
/// Initializes a new instance of the <see cref="User"/> class.
/// Public constructor with required data.
/// </summary>
/// <param name="username"></param>
/// <param name="mustUpdatePassword"></param>
/// <param name="authenticationProviderId"></param>
/// <param name="invalidLoginAttemptCount"></param>
/// <param name="subtitleMode"></param>
/// <param name="playDefaultAudioTrack"></param>
public User(
string username,
bool mustUpdatePassword,
string authenticationProviderId,
int invalidLoginAttemptCount,
SubtitlePlaybackMode subtitleMode,
bool playDefaultAudioTrack)
/// <param name="username">The username for the new user.</param>
/// <param name="authenticationProviderId">The authentication provider's Id</param>
public User(string username, string authenticationProviderId)
{
if (string.IsNullOrEmpty(username))
{
@ -60,11 +38,7 @@ namespace Jellyfin.Data.Entities
}
Username = username;
MustUpdatePassword = mustUpdatePassword;
AuthenticationProviderId = authenticationProviderId;
InvalidLoginAttemptCount = invalidLoginAttemptCount;
SubtitleMode = subtitleMode;
PlayDefaultAudioTrack = playDefaultAudioTrack;
Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
@ -74,6 +48,8 @@ namespace Jellyfin.Data.Entities
// Set default values
Id = Guid.NewGuid();
InvalidLoginAttemptCount = 0;
MustUpdatePassword = false;
DisplayMissingEpisodes = false;
DisplayCollectionsView = false;
HidePlayedInLatest = true;
@ -81,36 +57,40 @@ namespace Jellyfin.Data.Entities
RememberSubtitleSelections = true;
EnableNextEpisodeAutoPlay = true;
EnableAutoLogin = false;
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
AddDefaultPermissions();
AddDefaultPreferences();
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// Initializes a new instance of the <see cref="User"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
public static User CreateUserUnsafe()
protected User()
{
return new User();
Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
AccessSchedules = new HashSet<AccessSchedule>();
AddDefaultPermissions();
AddDefaultPreferences();
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="username"></param>
/// <param name="mustUpdatePassword"></param>
/// <param name="authenticationProviderId"></param>
/// <param name="invalidLoginAttemptCount"></param>
/// <param name="subtitleMode"></param>
/// <param name="playDefaultAudioTrack"></param>
public static User Create(
string username,
bool mustUpdatePassword,
string authenticationProviderId,
int invalidLoginAttemptCount,
SubtitlePlaybackMode subtitleMode,
bool playDefaultAudioTrack)
/// <param name="username">The username for the created user.</param>
/// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
/// <returns>The created instance.</returns>
public static User Create(string username, string authenticationProviderId)
{
return new User(username, mustUpdatePassword, authenticationProviderId, invalidLoginAttemptCount, subtitleMode, playDefaultAudioTrack);
return new User(username, authenticationProviderId);
}
/*************************************************************************
@ -131,7 +111,6 @@ namespace Jellyfin.Data.Entities
[Required]
[MaxLength(255)]
[StringLength(255)]
[JsonPropertyName("Name")]
public string Username { get; set; }
/// <summary>
@ -199,6 +178,7 @@ namespace Jellyfin.Data.Entities
public bool PlayDefaultAudioTrack { get; set; }
/// <summary>
/// Gets or sets the subtitle language preference.
/// Max length = 255
/// </summary>
[MaxLength(255)]
@ -237,6 +217,7 @@ namespace Jellyfin.Data.Entities
public int? RemoteClientBitrateLimit { get; set; }
/// <summary>
/// Gets or sets the internal id.
/// This is a temporary stopgap for until the library db is migrated.
/// This corresponds to the value of the index of this user in the library db.
/// </summary>
@ -246,7 +227,8 @@ namespace Jellyfin.Data.Entities
public ImageInfo ProfileImage { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Gets or sets the row version.
/// Required, ConcurrenyToken.
/// </summary>
[ConcurrencyCheck]
[Required]
@ -260,23 +242,25 @@ namespace Jellyfin.Data.Entities
/*************************************************************************
* Navigation properties
*************************************************************************/
[ForeignKey("Group_Groups_Id")]
[ForeignKey("Group_Groups_Guid")]
public ICollection<Group> Groups { get; protected set; }
[ForeignKey("Permission_Permissions_Id")]
[ForeignKey("Permission_Permissions_Guid")]
public ICollection<Permission> Permissions { get; protected set; }
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public ICollection<ProviderMapping> ProviderMappings { get; protected set; }
[ForeignKey("Preference_Preferences_Id")]
[ForeignKey("Preference_Preferences_Guid")]
public ICollection<Preference> Preferences { get; protected set; }
public ICollection<AccessSchedule> AccessSchedules { get; protected set; }
partial void Init();
public bool HasPermission(PermissionKind permission)
{
return Permissions.Select(p => p.Kind).Contains(permission);
return Permissions.First(p => p.Kind == permission).Value;
}
public void SetPermission(PermissionKind kind, bool value)
@ -287,11 +271,12 @@ namespace Jellyfin.Data.Entities
public string[] GetPreference(PreferenceKind preference)
{
return Preferences
var val = Preferences
.Where(p => p.Kind == preference)
.Select(p => p.Value)
.First()
.Split(Delimiter);
.First();
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
}
public void SetPreference(PreferenceKind preference, string[] values)
@ -332,5 +317,39 @@ namespace Jellyfin.Data.Entities
return hour >= schedule.StartHour && hour <= schedule.EndHour;
}
// TODO: make these user configurable?
private void AddDefaultPermissions()
{
Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
Permissions.Add(new Permission(PermissionKind.IsHidden, false));
Permissions.Add(new Permission(PermissionKind.EnableAllChannels, false));
Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true));
Permissions.Add(new Permission(PermissionKind.EnableAllFolders, false));
Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false));
Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true));
Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true));
Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true));
Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true));
Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true));
Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true));
Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
}
private void AddDefaultPreferences()
{
foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
{
Preferences.Add(new Preference(val, string.Empty));
}
}
}
}

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Jellyfin.Data.Entities;
namespace Jellyfin.Data
{
public interface IHasPermissions
{
ICollection<Permission> Permissions { get; }
}
}

@ -16,6 +16,13 @@ namespace Jellyfin.Server.Implementations
public partial class JellyfinDb : DbContext
{
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
public virtual DbSet<Group> Groups { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<Data.Entities.User> Users { get; set; }
/*public virtual DbSet<Artwork> Artwork { get; set; }
public virtual DbSet<Book> Books { get; set; }
@ -30,7 +37,6 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<Episode> Episodes { get; set; }
public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
public virtual DbSet<Genre> Genres { get; set; }
public virtual DbSet<Group> Groups { get; set; }
public virtual DbSet<Library> Libraries { get; set; }
public virtual DbSet<LibraryItem> LibraryItems { get; set; }
public virtual DbSet<LibraryRoot> LibraryRoot { get; set; }
@ -43,12 +49,10 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<PersonRole> PersonRoles { get; set; }
public virtual DbSet<Photo> Photo { get; set; }
public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
public virtual DbSet<Rating> Ratings { get; set; }

@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations
public JellyfinDbProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
serviceProvider.GetService<JellyfinDb>().Database.Migrate();
serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
}
/// <summary>

@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
#pragma warning disable SA1601
// <auto-generated />
@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
[Migration("20200504195702_UserSchema")]
partial class UserSchema
[Migration("20200517002411_AddUsers")]
partial class AddUsers
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
@ -22,6 +22,31 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.3");
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("AccessSchedule");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
@ -65,17 +90,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.ToTable("ActivityLog");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
{
b.Property<int>("Id")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
.HasColumnType("TEXT");
b.Property<int?>("Group_Groups_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Group_Groups_Guid")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
@ -88,9 +113,27 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("Group_Groups_Id");
b.HasIndex("Group_Groups_Guid");
b.ToTable("Groups");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.ToTable("Group");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ImageInfo");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@ -102,11 +145,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<int?>("Permission_GroupPermissions_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_GroupPermissions_Id")
.HasColumnType("TEXT");
b.Property<int?>("Permission_Permissions_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
@ -119,9 +162,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Permission_GroupPermissions_Id");
b.HasIndex("Permission_Permissions_Id");
b.HasIndex("Permission_Permissions_Guid");
b.ToTable("Permission");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@ -133,8 +176,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<int?>("Preference_Preferences_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<Guid?>("Preference_Preferences_Id")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
@ -147,9 +193,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("Preference_Preferences_Guid");
b.HasIndex("Preference_Preferences_Id");
b.ToTable("Preference");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b =>
@ -163,8 +211,8 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<int?>("ProviderMapping_ProviderMappings_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("ProviderMapping_ProviderMappings_Id")
.HasColumnType("TEXT");
b.Property<string>("ProviderName")
.IsRequired()
@ -189,12 +237,11 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<int>("Id")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
@ -203,71 +250,86 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<bool?>("DisplayCollectionsView")
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool?>("DisplayMissingEpisodes")
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool?>("EnableNextEpisodeAutoPlay")
b.Property<string>("EasyPassword")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool?>("EnableUserPreferenceAccess")
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<string>("GroupedFolders")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool?>("HidePlayedInLatest")
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<string>("LatestItemExcludes")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<DateTime>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<string>("MyMediaExcludes")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("OrderedViews")
b.Property<string>("Password")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<string>("Password")
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(65535);
.HasMaxLength(255);
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool?>("RememberAudioSelections")
b.Property<int?>("ProfileImageId")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool?>("RememberSubtitleSelections")
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePrefernce")
b.Property<string>("SubtitleLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<string>("SubtitleMode")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
@ -276,34 +338,45 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.ToTable("User");
b.HasIndex("ProfileImageId");
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.Group", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Groups")
.HasForeignKey("Group_Groups_Id");
.HasForeignKey("Group_Groups_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.Group", null)
.WithMany("GroupPermissions")
.WithMany("Permissions")
.HasForeignKey("Permission_GroupPermissions_Id");
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("Permission_Permissions_Id");
.HasForeignKey("Permission_Permissions_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.Group", null)
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Id");
.HasForeignKey("Preference_Preferences_Guid");
b.HasOne("Jellyfin.Data.Entities.User", null)
b.HasOne("Jellyfin.Data.Entities.Group", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Id");
});
@ -318,6 +391,13 @@ namespace Jellyfin.Server.Implementations.Migrations
.WithMany("ProviderMappings")
.HasForeignKey("ProviderMapping_ProviderMappings_Id");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage")
.WithMany()
.HasForeignKey("ProfileImageId");
});
#pragma warning restore 612, 618
}
}

@ -1,74 +1,125 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
#pragma warning disable SA1601
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class UserSchema : Migration
public partial class AddUsers : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "User",
name: "ImageInfo",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Path = table.Column<string>(nullable: false),
LastModified = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ImageInfo", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
Username = table.Column<string>(maxLength: 255, nullable: false),
Password = table.Column<string>(maxLength: 65535, nullable: true),
EasyPassword = table.Column<string>(maxLength: 65535, nullable: true),
MustUpdatePassword = table.Column<bool>(nullable: false),
AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: false),
AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
AuthenticationProviderId = table.Column<string>(maxLength: 255, nullable: false),
GroupedFolders = table.Column<string>(maxLength: 65535, nullable: true),
PasswordResetProviderId = table.Column<string>(maxLength: 255, nullable: false),
InvalidLoginAttemptCount = table.Column<int>(nullable: false),
LatestItemExcludes = table.Column<string>(maxLength: 65535, nullable: true),
LastActivityDate = table.Column<DateTime>(nullable: false),
LastLoginDate = table.Column<DateTime>(nullable: false),
LoginAttemptsBeforeLockout = table.Column<int>(nullable: true),
MyMediaExcludes = table.Column<string>(maxLength: 65535, nullable: true),
OrderedViews = table.Column<string>(maxLength: 65535, nullable: true),
SubtitleMode = table.Column<string>(maxLength: 255, nullable: false),
SubtitleMode = table.Column<int>(nullable: false),
PlayDefaultAudioTrack = table.Column<bool>(nullable: false),
SubtitleLanguagePrefernce = table.Column<string>(maxLength: 255, nullable: true),
DisplayMissingEpisodes = table.Column<bool>(nullable: true),
DisplayCollectionsView = table.Column<bool>(nullable: true),
HidePlayedInLatest = table.Column<bool>(nullable: true),
RememberAudioSelections = table.Column<bool>(nullable: true),
RememberSubtitleSelections = table.Column<bool>(nullable: true),
EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: true),
EnableUserPreferenceAccess = table.Column<bool>(nullable: true),
SubtitleLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
DisplayMissingEpisodes = table.Column<bool>(nullable: false),
DisplayCollectionsView = table.Column<bool>(nullable: false),
EnableLocalPassword = table.Column<bool>(nullable: false),
HidePlayedInLatest = table.Column<bool>(nullable: false),
RememberAudioSelections = table.Column<bool>(nullable: false),
RememberSubtitleSelections = table.Column<bool>(nullable: false),
EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: false),
EnableAutoLogin = table.Column<bool>(nullable: false),
EnableUserPreferenceAccess = table.Column<bool>(nullable: false),
MaxParentalAgeRating = table.Column<int>(nullable: true),
RemoteClientBitrateLimit = table.Column<int>(nullable: true),
InternalId = table.Column<long>(nullable: false),
ProfileImageId = table.Column<int>(nullable: true),
RowVersion = table.Column<uint>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_User", x => x.Id);
table.PrimaryKey("PK_Users", x => x.Id);
table.ForeignKey(
name: "FK_Users_ImageInfo_ProfileImageId",
column: x => x.ProfileImageId,
principalSchema: "jellyfin",
principalTable: "ImageInfo",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Group",
name: "AccessSchedule",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(nullable: false),
DayOfWeek = table.Column<int>(nullable: false),
StartHour = table.Column<double>(nullable: false),
EndHour = table.Column<double>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccessSchedule", x => x.Id);
table.ForeignKey(
name: "FK_AccessSchedule_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Groups",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
Name = table.Column<string>(maxLength: 255, nullable: false),
RowVersion = table.Column<uint>(nullable: false),
Group_Groups_Id = table.Column<int>(nullable: true)
Group_Groups_Guid = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Group", x => x.Id);
table.PrimaryKey("PK_Groups", x => x.Id);
table.ForeignKey(
name: "FK_Group_User_Group_Groups_Id",
column: x => x.Group_Groups_Id,
name: "FK_Groups_Users_Group_Groups_Guid",
column: x => x.Group_Groups_Guid,
principalSchema: "jellyfin",
principalTable: "User",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Permission",
name: "Permissions",
schema: "jellyfin",
columns: table => new
{
@ -77,30 +128,30 @@ namespace Jellyfin.Server.Implementations.Migrations
Kind = table.Column<int>(nullable: false),
Value = table.Column<bool>(nullable: false),
RowVersion = table.Column<uint>(nullable: false),
Permission_GroupPermissions_Id = table.Column<int>(nullable: true),
Permission_Permissions_Id = table.Column<int>(nullable: true)
Permission_GroupPermissions_Id = table.Column<Guid>(nullable: true),
Permission_Permissions_Guid = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Permission", x => x.Id);
table.PrimaryKey("PK_Permissions", x => x.Id);
table.ForeignKey(
name: "FK_Permission_Group_Permission_GroupPermissions_Id",
name: "FK_Permissions_Groups_Permission_GroupPermissions_Id",
column: x => x.Permission_GroupPermissions_Id,
principalSchema: "jellyfin",
principalTable: "Group",
principalTable: "Groups",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Permission_User_Permission_Permissions_Id",
column: x => x.Permission_Permissions_Id,
name: "FK_Permissions_Users_Permission_Permissions_Guid",
column: x => x.Permission_Permissions_Guid,
principalSchema: "jellyfin",
principalTable: "User",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Preference",
name: "Preferences",
schema: "jellyfin",
columns: table => new
{
@ -109,23 +160,24 @@ namespace Jellyfin.Server.Implementations.Migrations
Kind = table.Column<int>(nullable: false),
Value = table.Column<string>(maxLength: 65535, nullable: false),
RowVersion = table.Column<uint>(nullable: false),
Preference_Preferences_Id = table.Column<int>(nullable: true)
Preference_Preferences_Guid = table.Column<Guid>(nullable: true),
Preference_Preferences_Id = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Preference", x => x.Id);
table.PrimaryKey("PK_Preferences", x => x.Id);
table.ForeignKey(
name: "FK_Preference_Group_Preference_Preferences_Id",
column: x => x.Preference_Preferences_Id,
name: "FK_Preferences_Users_Preference_Preferences_Guid",
column: x => x.Preference_Preferences_Guid,
principalSchema: "jellyfin",
principalTable: "Group",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Preference_User_Preference_Preferences_Id",
name: "FK_Preferences_Groups_Preference_Preferences_Id",
column: x => x.Preference_Preferences_Id,
principalSchema: "jellyfin",
principalTable: "User",
principalTable: "Groups",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
@ -141,49 +193,61 @@ namespace Jellyfin.Server.Implementations.Migrations
ProviderSecrets = table.Column<string>(maxLength: 65535, nullable: false),
ProviderData = table.Column<string>(maxLength: 65535, nullable: false),
RowVersion = table.Column<uint>(nullable: false),
ProviderMapping_ProviderMappings_Id = table.Column<int>(nullable: true)
ProviderMapping_ProviderMappings_Id = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProviderMapping", x => x.Id);
table.ForeignKey(
name: "FK_ProviderMapping_Group_ProviderMapping_ProviderMappings_Id",
name: "FK_ProviderMapping_Groups_ProviderMapping_ProviderMappings_Id",
column: x => x.ProviderMapping_ProviderMappings_Id,
principalSchema: "jellyfin",
principalTable: "Group",
principalTable: "Groups",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_ProviderMapping_User_ProviderMapping_ProviderMappings_Id",
name: "FK_ProviderMapping_Users_ProviderMapping_ProviderMappings_Id",
column: x => x.ProviderMapping_ProviderMappings_Id,
principalSchema: "jellyfin",
principalTable: "User",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Group_Group_Groups_Id",
name: "IX_AccessSchedule_UserId",
schema: "jellyfin",
table: "Group",
column: "Group_Groups_Id");
table: "AccessSchedule",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Permission_Permission_GroupPermissions_Id",
name: "IX_Groups_Group_Groups_Guid",
schema: "jellyfin",
table: "Permission",
table: "Groups",
column: "Group_Groups_Guid");
migrationBuilder.CreateIndex(
name: "IX_Permissions_Permission_GroupPermissions_Id",
schema: "jellyfin",
table: "Permissions",
column: "Permission_GroupPermissions_Id");
migrationBuilder.CreateIndex(
name: "IX_Permission_Permission_Permissions_Id",
name: "IX_Permissions_Permission_Permissions_Guid",
schema: "jellyfin",
table: "Permissions",
column: "Permission_Permissions_Guid");
migrationBuilder.CreateIndex(
name: "IX_Preferences_Preference_Preferences_Guid",
schema: "jellyfin",
table: "Permission",
column: "Permission_Permissions_Id");
table: "Preferences",
column: "Preference_Preferences_Guid");
migrationBuilder.CreateIndex(
name: "IX_Preference_Preference_Preferences_Id",
name: "IX_Preferences_Preference_Preferences_Id",
schema: "jellyfin",
table: "Preference",
table: "Preferences",
column: "Preference_Preferences_Id");
migrationBuilder.CreateIndex(
@ -191,16 +255,26 @@ namespace Jellyfin.Server.Implementations.Migrations
schema: "jellyfin",
table: "ProviderMapping",
column: "ProviderMapping_ProviderMappings_Id");
migrationBuilder.CreateIndex(
name: "IX_Users_ProfileImageId",
schema: "jellyfin",
table: "Users",
column: "ProfileImageId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Permission",
name: "AccessSchedule",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Permissions",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Preference",
name: "Preferences",
schema: "jellyfin");
migrationBuilder.DropTable(
@ -208,11 +282,15 @@ namespace Jellyfin.Server.Implementations.Migrations
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Group",
name: "Groups",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Users",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "User",
name: "ImageInfo",
schema: "jellyfin");
}
}

@ -1,7 +1,9 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
@ -15,6 +17,31 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.3");
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("AccessSchedule");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
@ -63,12 +90,12 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
{
b.Property<int>("Id")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
.HasColumnType("TEXT");
b.Property<int?>("Group_Groups_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Group_Groups_Guid")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
@ -81,9 +108,27 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("Group_Groups_Id");
b.HasIndex("Group_Groups_Guid");
b.ToTable("Groups");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.ToTable("Group");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ImageInfo");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@ -95,11 +140,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<int?>("Permission_GroupPermissions_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_GroupPermissions_Id")
.HasColumnType("TEXT");
b.Property<int?>("Permission_Permissions_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
@ -112,9 +157,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Permission_GroupPermissions_Id");
b.HasIndex("Permission_Permissions_Id");
b.HasIndex("Permission_Permissions_Guid");
b.ToTable("Permission");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@ -126,8 +171,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<int?>("Preference_Preferences_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<Guid?>("Preference_Preferences_Id")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
@ -140,9 +188,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("Preference_Preferences_Guid");
b.HasIndex("Preference_Preferences_Id");
b.ToTable("Preference");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b =>
@ -156,8 +206,8 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<int?>("ProviderMapping_ProviderMappings_Id")
.HasColumnType("INTEGER");
b.Property<Guid?>("ProviderMapping_ProviderMappings_Id")
.HasColumnType("TEXT");
b.Property<string>("ProviderName")
.IsRequired()
@ -182,12 +232,11 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<int>("Id")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
@ -196,71 +245,86 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<bool?>("DisplayCollectionsView")
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool?>("DisplayMissingEpisodes")
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool?>("EnableNextEpisodeAutoPlay")
b.Property<string>("EasyPassword")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool?>("EnableUserPreferenceAccess")
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<string>("GroupedFolders")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool?>("HidePlayedInLatest")
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<string>("LatestItemExcludes")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<DateTime>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<string>("MyMediaExcludes")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("OrderedViews")
b.Property<string>("Password")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<string>("Password")
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(65535);
.HasMaxLength(255);
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool?>("RememberAudioSelections")
b.Property<int?>("ProfileImageId")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool?>("RememberSubtitleSelections")
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePrefernce")
b.Property<string>("SubtitleLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<string>("SubtitleMode")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
@ -269,34 +333,45 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.ToTable("User");
b.HasIndex("ProfileImageId");
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.Group", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Groups")
.HasForeignKey("Group_Groups_Id");
.HasForeignKey("Group_Groups_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.Group", null)
.WithMany("GroupPermissions")
.WithMany("Permissions")
.HasForeignKey("Permission_GroupPermissions_Id");
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("Permission_Permissions_Id");
.HasForeignKey("Permission_Permissions_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.Group", null)
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Id");
.HasForeignKey("Preference_Preferences_Guid");
b.HasOne("Jellyfin.Data.Entities.User", null)
b.HasOne("Jellyfin.Data.Entities.Group", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Id");
});
@ -311,6 +386,13 @@ namespace Jellyfin.Server.Implementations.Migrations
.WithMany("ProviderMappings")
.HasForeignKey("ProviderMapping_ProviderMappings_Id");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage")
.WithMany()
.HasForeignKey("ProfileImageId");
});
#pragma warning restore 612, 618
}
}

@ -7,7 +7,7 @@ using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Cryptography;
namespace Jellyfin.Server.Implementations.User
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default authentication provider.

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
@ -10,7 +11,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.User
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default password reset provider.
@ -94,7 +95,7 @@ namespace Jellyfin.Server.Implementations.User
}
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(Jellyfin.Data.Entities.User user, bool isInNetwork)
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
string pin;
using (var cryptoRandom = RandomNumberGenerator.Create())

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@ -9,7 +10,7 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
namespace Jellyfin.Server.Implementations.User
namespace Jellyfin.Server.Implementations.Users
{
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
@ -33,7 +34,11 @@ namespace Jellyfin.Server.Implementations.User
return Task.CompletedTask;
}
private void OnUserUpdated(object sender, GenericEventArgs<Data.Entities.User> e)
public void Dispose()
{
}
private void OnUserUpdated(object sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
@ -42,11 +47,7 @@ namespace Jellyfin.Server.Implementations.User
}
}
public void Dispose()
{
}
private void UpdateDeviceAccess(Data.Entities.User user)
private void UpdateDeviceAccess(User user)
{
var existing = _authRepo.Get(new AuthenticationInfoQuery
{

@ -1,7 +1,7 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
namespace Jellyfin.Server.Implementations.User
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// An invalid authentication provider.

@ -1,4 +1,4 @@
#pragma warning disable CS0067
#pragma warning disable CA1307
#pragma warning disable CS1591
using System;
@ -6,7 +6,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Net;
@ -20,7 +22,7 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.User
namespace Jellyfin.Server.Implementations.Users
{
public class UserManager : IUserManager
{
@ -47,24 +49,24 @@ namespace Jellyfin.Server.Implementations.User
_logger = logger;
}
public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserPasswordChanged;
public event EventHandler<GenericEventArgs<User>> OnUserPasswordChanged;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserUpdated;
public event EventHandler<GenericEventArgs<User>> OnUserUpdated;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserCreated;
public event EventHandler<GenericEventArgs<User>> OnUserCreated;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserDeleted;
public event EventHandler<GenericEventArgs<User>> OnUserDeleted;
public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserLockedOut;
public event EventHandler<GenericEventArgs<User>> OnUserLockedOut;
public IEnumerable<Data.Entities.User> Users
public IEnumerable<User> Users
{
get
{
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
return dbContext.Users;
}
}
@ -73,37 +75,38 @@ namespace Jellyfin.Server.Implementations.User
{
get
{
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
return dbContext.Users.Select(u => u.Id);
}
}
public Data.Entities.User GetUserById(Guid id)
public User GetUserById(Guid id)
{
if (id == Guid.Empty)
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
return dbContext.Users.Find(id);
}
public Data.Entities.User GetUserByName(string name)
public User GetUserByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Invalid username", nameof(name));
}
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
return dbContext.Users.FirstOrDefault(u =>
string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
// This can't use an overload with StringComparer because that would cause the query to
// have to be evaluated client-side.
return dbContext.Users.FirstOrDefault(u => string.Equals(u.Username, name));
}
public async Task RenameUser(Data.Entities.User user, string newName)
public async Task RenameUser(User user, string newName)
{
if (user == null)
{
@ -132,43 +135,50 @@ namespace Jellyfin.Server.Implementations.User
user.Username = newName;
await UpdateUserAsync(user).ConfigureAwait(false);
OnUserUpdated?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
}
public void UpdateUser(Data.Entities.User user)
public void UpdateUser(User user)
{
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
dbContext.SaveChanges();
}
public async Task UpdateUserAsync(Data.Entities.User user)
public async Task UpdateUserAsync(User user)
{
await using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
public Data.Entities.User CreateUser(string name)
public User CreateUser(string name)
{
using var dbContext = _dbProvider.CreateContext();
if (!IsValidUsername(name))
{
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
var dbContext = _dbProvider.CreateContext();
var newUser = CreateUserObject(name);
var newUser = new User(name, _defaultAuthenticationProvider.GetType().FullName);
dbContext.Users.Add(newUser);
dbContext.SaveChanges();
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
return newUser;
}
public void DeleteUser(Data.Entities.User user)
public void DeleteUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
using var dbContext = _dbProvider.CreateContext();
var dbContext = _dbProvider.CreateContext();
if (!dbContext.Users.Contains(user))
{
@ -200,19 +210,20 @@ namespace Jellyfin.Server.Implementations.User
dbContext.Users.Remove(user);
dbContext.SaveChanges();
OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
}
public Task ResetPassword(Data.Entities.User user)
public Task ResetPassword(User user)
{
return ChangePassword(user, string.Empty);
}
public void ResetEasyPassword(Data.Entities.User user)
public void ResetEasyPassword(User user)
{
ChangeEasyPassword(user, string.Empty, null);
}
public async Task ChangePassword(Data.Entities.User user, string newPassword)
public async Task ChangePassword(User user, string newPassword)
{
if (user == null)
{
@ -222,24 +233,18 @@ namespace Jellyfin.Server.Implementations.User
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
}
public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordSha1)
public void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
UpdateUser(user);
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
}
public UserDto GetUserDto(Data.Entities.User user, string remoteEndPoint = null)
public UserDto GetUserDto(User user, string remoteEndPoint = null)
{
return new UserDto
{
@ -271,7 +276,7 @@ namespace Jellyfin.Server.Implementations.User
MaxParentalRating = user.MaxParentalAgeRating,
EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(),
AuthenticatioIsnProviderId = user.AuthenticationProviderId,
AuthenticationProviderId = user.AuthenticationProviderId,
PasswordResetProviderId = user.PasswordResetProviderId,
InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(),
@ -306,7 +311,7 @@ namespace Jellyfin.Server.Implementations.User
};
}
public PublicUserDto GetPublicUserDto(Data.Entities.User user, string remoteEndPoint = null)
public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null)
{
if (user == null)
{
@ -328,7 +333,7 @@ namespace Jellyfin.Server.Implementations.User
};
}
public async Task<Data.Entities.User> AuthenticateUser(
public async Task<User> AuthenticateUser(
string username,
string password,
string passwordSha1,
@ -341,7 +346,7 @@ namespace Jellyfin.Server.Implementations.User
throw new ArgumentNullException(nameof(username));
}
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
bool success;
IAuthenticationProvider authenticationProvider;
@ -370,7 +375,7 @@ namespace Jellyfin.Server.Implementations.User
// Search the database for the user again
// the authentication provider might have created it
user = Users
.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
{
@ -436,10 +441,10 @@ namespace Jellyfin.Server.Implementations.User
if (isUserSession)
{
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
UpdateUser(user);
await UpdateUserAsync(user).ConfigureAwait(false);
}
ResetInvalidLoginAttemptCount(user);
user.InvalidLoginAttemptCount = 0;
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
}
else
@ -495,14 +500,11 @@ namespace Jellyfin.Server.Implementations.User
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
{
_authenticationProviders = authenticationProviders.ToArray();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_passwordResetProviders = passwordResetProviders.ToArray();
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
}
public NameIdPair[] GetAuthenticationProviders()
@ -563,7 +565,7 @@ namespace Jellyfin.Server.Implementations.User
user.MaxParentalAgeRating = policy.MaxParentalRating;
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
user.AuthenticationProviderId = policy.AuthenticatioIsnProviderId;
user.AuthenticationProviderId = policy.AuthenticationProviderId;
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1
@ -604,28 +606,25 @@ namespace Jellyfin.Server.Implementations.User
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
}
private Data.Entities.User CreateUserObject(string name)
private bool IsValidUsername(string name)
{
return new Data.Entities.User(
username: name,
mustUpdatePassword: false,
authenticationProviderId: _defaultAuthenticationProvider.GetType().FullName,
invalidLoginAttemptCount: -1,
subtitleMode: SubtitlePlaybackMode.Default,
playDefaultAudioTrack: true);
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
return Regex.IsMatch(name, @"^[\w\-'._@]*$");
}
private IAuthenticationProvider GetAuthenticationProvider(Data.Entities.User user)
private IAuthenticationProvider GetAuthenticationProvider(User user)
{
return GetAuthenticationProviders(user)[0];
}
private IPasswordResetProvider GetPasswordResetProvider(Data.Entities.User user)
private IPasswordResetProvider GetPasswordResetProvider(User user)
{
return GetPasswordResetProviders(user)[0];
}
private IList<IAuthenticationProvider> GetAuthenticationProviders(Data.Entities.User user)
private IList<IAuthenticationProvider> GetAuthenticationProviders(User user)
{
var authenticationProviderId = user?.AuthenticationProviderId;
@ -640,7 +639,7 @@ namespace Jellyfin.Server.Implementations.User
{
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
_logger.LogWarning(
"User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
"User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
user?.Username,
user?.AuthenticationProviderId);
providers = new List<IAuthenticationProvider>
@ -652,7 +651,7 @@ namespace Jellyfin.Server.Implementations.User
return providers;
}
private IList<IPasswordResetProvider> GetPasswordResetProviders(Data.Entities.User user)
private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
{
var passwordResetProviderId = user?.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
@ -675,11 +674,10 @@ namespace Jellyfin.Server.Implementations.User
return providers;
}
private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)>
AuthenticateLocalUser(
private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(
string username,
string password,
Jellyfin.Data.Entities.User user,
User user,
string remoteEndPoint)
{
bool success = false;
@ -721,7 +719,7 @@ namespace Jellyfin.Server.Implementations.User
IAuthenticationProvider provider,
string username,
string password,
Data.Entities.User resolvedUser)
User resolvedUser)
{
try
{
@ -745,27 +743,21 @@ namespace Jellyfin.Server.Implementations.User
}
}
private void IncrementInvalidLoginAttemptCount(Data.Entities.User user)
private void IncrementInvalidLoginAttemptCount(User user)
{
int invalidLogins = user.InvalidLoginAttemptCount;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
if (maxInvalidLogins.HasValue
&& invalidLogins >= maxInvalidLogins)
if (maxInvalidLogins.HasValue && invalidLogins >= maxInvalidLogins)
{
user.SetPermission(PermissionKind.IsDisabled, true);
OnUserLockedOut?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
_logger.LogWarning(
"Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
"Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
user.Username,
invalidLogins);
}
UpdateUser(user);
}
private void ResetInvalidLoginAttemptCount(Data.Entities.User user)
{
user.InvalidLoginAttemptCount = 0;
}
}
}

@ -7,8 +7,10 @@ using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@ -69,6 +71,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
base.RegisterServices(serviceCollection);
}
@ -80,6 +83,9 @@ namespace Jellyfin.Server
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
yield return typeof(CoreAppHost).Assembly;
yield return typeof(DefaultAuthenticationProvider).Assembly;
yield return typeof(DefaultPasswordResetProvider).Assembly;
yield return typeof(InvalidAuthProvider).Assembly;
}
/// <inheritdoc />

@ -41,7 +41,6 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="Json.Net" Version="1.0.22" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
<PackageReference Include="prometheus-net" Version="3.5.0" />

@ -1,33 +1,45 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System;
using System.IO;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Users;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using SQLitePCL.pretty;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// The migration routine for migrating the user database to EF Core.
/// </summary>
public class MigrateUserDb : IMigrationRoutine
{
private readonly ILogger<MigrateUserDb> _logger;
private const string DbFilename = "users.db";
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
private readonly MyXmlSerializer _xmlSerializer;
public MigrateUserDb(ILogger<MigrateUserDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider, MyXmlSerializer xmlSerializer)
/// <summary>
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
/// <param name="xmlSerializer">The xml serializer.</param>
public MigrateUserDb(
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
MyXmlSerializer xmlSerializer)
{
_logger = logger;
_paths = paths;
@ -35,18 +47,21 @@ namespace Jellyfin.Server.Migrations.Routines
_xmlSerializer = xmlSerializer;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
public string Name => "MigrateUserDb";
/// <inheritdoc/>
public string Name => "MigrateUserDatabase";
/// <inheritdoc/>
public void Perform()
{
var dataPath = _paths.DataPath;
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
using (var connection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null))
using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
{
using var dbContext = _provider.CreateContext();
var dbContext = _provider.CreateContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
@ -55,26 +70,30 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var entry in queryResult)
{
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(entry[2].ToString());
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, json["Name"]);
var config = (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"));
var policy = (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"));
var user = new User(
json["Name"],
false,
policy.AuthenticatioIsnProviderId,
policy.InvalidLoginAttemptCount,
config.SubtitleMode,
config.PlayDefaultAudioTrack)
UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob());
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
: new UserConfiguration();
var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
: new UserPolicy();
policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
"Emby.Server.Implementations.Library",
"Jellyfin.Server.Implementations.Users",
StringComparison.Ordinal)
?? typeof(DefaultAuthenticationProvider).FullName;
policy.PasswordResetProviderId ??= typeof(DefaultPasswordResetProvider).FullName;
var user = new User(mockup.Name, policy.AuthenticationProviderId)
{
Id = entry[1].ReadGuidFromBlob(),
InternalId = entry[0].ToInt64(),
MaxParentalAgeRating = policy.MaxParentalRating,
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
AuthenticationProviderId = policy.AuthenticatioIsnProviderId,
PasswordResetProviderId = policy.PasswordResetProviderId,
InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 ? null : new int?(policy.LoginAttemptsBeforeLockout),
@ -89,6 +108,10 @@ namespace Jellyfin.Server.Migrations.Routines
EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = config.RememberSubtitleSelections,
SubtitleLanguagePreference = config.SubtitleLanguagePreference,
Password = mockup.Password,
EasyPassword = mockup.EasyPassword,
LastLoginDate = mockup.LastLoginDate ?? DateTime.MinValue,
LastActivityDate = mockup.LastActivityDate ?? DateTime.MinValue
};
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
@ -112,6 +135,7 @@ namespace Jellyfin.Server.Migrations.Routines
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
user.AccessSchedules.Add(policyAccessSchedule);
@ -126,6 +150,8 @@ namespace Jellyfin.Server.Migrations.Routines
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Users.Add(user);
}
dbContext.SaveChanges();
@ -133,12 +159,32 @@ namespace Jellyfin.Server.Migrations.Routines
try
{
File.Move(Path.Combine(dataPath, "users.db"), Path.Combine(dataPath, "users.db" + ".old"));
File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
if (File.Exists(journalPath))
{
File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
}
}
catch (IOException e)
{
_logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
}
}
#nullable disable
internal class UserMockup
{
public string Password { get; set; }
public string EasyPassword { get; set; }
public DateTime? LastLoginDate { get; set; }
public DateTime? LastActivityDate { get; set; }
public string Name { get; set; }
}
}
}

@ -1,6 +1,8 @@
#pragma warning disable CS1591
using System;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@ -34,7 +36,7 @@ namespace MediaBrowser.Model.Users
public string[] BlockedTags { get; set; }
public bool EnableUserPreferenceAccess { get; set; }
public Jellyfin.Data.Entities.AccessSchedule[] AccessSchedules { get; set; }
public AccessSchedule[] AccessSchedules { get; set; }
public UnratedItem[] BlockUnratedItems { get; set; }
public bool EnableRemoteControlOfOtherUsers { get; set; }
public bool EnableSharedDeviceControl { get; set; }
@ -78,7 +80,9 @@ namespace MediaBrowser.Model.Users
public string[] BlockedChannels { get; set; }
public int RemoteClientBitrateLimit { get; set; }
public string AuthenticatioIsnProviderId { get; set; }
[XmlElement(ElementName = "AuthenticationProviderId")]
public string AuthenticationProviderId { get; set; }
public string PasswordResetProviderId { get; set; }
public UserPolicy()

Loading…
Cancel
Save