Merge pull request #6201 from barronpm/authenticationdb-efcore

Migrate Authentication DB to EF Core
pull/6499/head
Claus Vium 3 years ago committed by GitHub
commit fb5385f1df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -173,7 +173,9 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
var sessionInfo = await _sessionManager
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
.ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();

@ -38,7 +38,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@ -59,7 +58,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@ -75,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@ -595,8 +592,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
@ -618,8 +613,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
@ -655,8 +648,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
@ -685,8 +677,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();

@ -1,146 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
{
_userManager = userManager;
_authRepo = authRepo;
}
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_capabilitiesMap[deviceId] = capabilities;
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
_authRepo.UpdateDeviceOptions(deviceId, options);
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
return _authRepo.GetDeviceOptions(deviceId);
}
public ClientCapabilities GetCapabilities(string id)
{
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
? result
: new ClientCapabilities();
}
public DeviceInfo GetDevice(string id)
{
var session = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = id
}).Items.FirstOrDefault();
var device = session == null ? null : ToDeviceInfo(session);
return device;
}
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
{
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
// UserId = query.UserId
HasUser = true
}).Items;
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
if (query.SupportsSync.HasValue)
{
var val = query.SupportsSync.Value;
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
}
if (!query.UserId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(query.UserId);
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
var array = sessions.Select(ToDeviceInfo).ToArray();
return new QueryResult<DeviceInfo>(array);
}
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
{
var caps = GetCapabilities(authInfo.DeviceId);
return new DeviceInfo
{
AppName = authInfo.AppName,
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
LastUserName = authInfo.UserName,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps?.IconUrl
};
}
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
{
throw new ArgumentException("user not found");
}
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
{
return false;
}
}
return true;
}
}
}

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
public AuthorizationInfo Authenticate(HttpRequest request)
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public SessionInfo GetSession(HttpContext requestContext)
public async Task<SessionInfo> GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
return await _sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
requestContext.GetNormalizedRemoteIp().ToString(),
user).ConfigureAwait(false);
}
public SessionInfo GetSession(object requestContext)
public Task<SessionInfo> GetSession(object requestContext)
{
return GetSession((HttpContext)requestContext);
}
public User? GetUser(HttpContext requestContext)
public async Task<User?> GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
var session = await GetSession(requestContext).ConfigureAwait(false);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
public User? GetUser(object requestContext)
public Task<User?> GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}

@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
_ = _authService.Authenticate(context.Request);
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

@ -3,12 +3,13 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.QuickConnect;
using Microsoft.Extensions.Logging;
@ -19,11 +20,6 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
public class QuickConnectManager : IQuickConnect, IDisposable
{
/// <summary>
/// The name of internal access tokens.
/// </summary>
private const string TokenName = "QuickConnect";
/// <summary>
/// The length of user facing codes.
/// </summary>
@ -34,13 +30,13 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
private const int Timeout = 10;
private readonly RNGCryptoServiceProvider _rng = new();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new();
private readonly RNGCryptoServiceProvider _rng = new ();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
private readonly IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authenticationRepository;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
@ -48,18 +44,15 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
/// <param name="config">Configuration.</param>
/// <param name="logger">Logger.</param>
/// <param name="appHost">Application host.</param>
/// <param name="authenticationRepository">Authentication repository.</param>
/// <param name="sessionManager">Session Manager.</param>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
IServerApplicationHost appHost,
IAuthenticationRepository authenticationRepository)
ISessionManager sessionManager)
{
_config = config;
_logger = logger;
_appHost = appHost;
_authenticationRepository = authenticationRepository;
_sessionManager = sessionManager;
}
/// <inheritdoc />
@ -77,14 +70,41 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
public QuickConnectResult TryConnect()
public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
{
if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
{
throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Device))
{
throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Client))
{
throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Version))
{
throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
}
AssertActive();
ExpireRequests();
var secret = GenerateSecureRandom();
var code = GenerateCode();
var result = new QuickConnectResult(secret, code, DateTime.UtcNow);
var result = new QuickConnectResult(
secret,
code,
DateTime.UtcNow,
authorizationInfo.DeviceId,
authorizationInfo.Device,
authorizationInfo.Client,
authorizationInfo.Version);
_currentRequests[code] = result;
return result;
@ -129,7 +149,7 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
public bool AuthorizeRequest(Guid userId, string code)
public async Task<bool> AuthorizeRequest(Guid userId, string code)
{
AssertActive();
ExpireRequests();
@ -144,28 +164,41 @@ namespace Emby.Server.Implementations.QuickConnect
throw new InvalidOperationException("Request is already authorized");
}
var token = Guid.NewGuid();
result.Authentication = token;
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
result.DateAdded = DateTime.Now.Add(TimeSpan.FromMinutes(1));
result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
_authenticationRepository.Create(new AuthenticationInfo
var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
{
AppName = TokenName,
AccessToken = token.ToString("N", CultureInfo.InvariantCulture),
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString,
UserId = userId
});
UserId = userId,
DeviceId = result.DeviceId,
DeviceName = result.DeviceName,
App = result.AppName,
AppVersion = result.AppVersion
}).ConfigureAwait(false);
_authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
result.Authenticated = true;
_currentRequests[code] = result;
_logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
_logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
return true;
}
/// <inheritdoc/>
public AuthenticationResult GetAuthorizedRequest(string secret)
{
AssertActive();
ExpireRequests();
if (!_authorizedSecrets.TryGetValue(secret, out var result))
{
throw new ResourceNotFoundException("Unable to find request");
}
return result.AuthenticationResult;
}
/// <summary>
/// Dispose.
/// </summary>
@ -218,6 +251,18 @@ namespace Emby.Server.Implementations.QuickConnect
}
}
}
foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
{
if (expireAll || timestamp < minTime)
{
_logger.LogDebug("Removing expired secret {Secret}", secret);
if (!_authorizedSecrets.TryRemove(secret, out _))
{
_logger.LogWarning("Secret {Secret} already expired", secret);
}
}
}
}
}
}

@ -1,408 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Security
{
public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
{
public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
: base(logger)
{
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
}
public void Initialize()
{
string[] queries =
{
"create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
"create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
"drop index if exists idx_AccessTokens",
"drop index if exists Tokens1",
"drop index if exists Tokens2",
"create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
"create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
"create index if not exists Devices1 on Devices (Id)"
};
using (var connection = GetConnection())
{
var tableNewlyCreated = !TableExists(connection, "Tokens");
connection.RunQueries(queries);
TryMigrate(connection, tableNewlyCreated);
}
}
private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated)
{
try
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
connection.RunInTransaction(
db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames);
AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames);
AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
}, TransactionMode);
connection.RunQueries(new[]
{
"update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null",
"update accesstokens set DeviceName='Unknown' where DeviceName is null",
"update accesstokens set AppName='Unknown' where AppName is null",
"update accesstokens set AppVersion='1' where AppVersion is null",
"INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1"
});
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error migrating authentication database");
}
}
public void Create(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Update(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Delete(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.MoveNext();
}
}, TransactionMode);
}
}
private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id";
private static void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement)
{
if (!string.IsNullOrEmpty(query.AccessToken))
{
statement.TryBind("@AccessToken", query.AccessToken);
}
if (!query.UserId.Equals(Guid.Empty))
{
statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
statement.TryBind("@DeviceId", query.DeviceId);
}
}
public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var commandText = BaseSelectText;
var whereClauses = new List<string>();
if (!string.IsNullOrEmpty(query.AccessToken))
{
whereClauses.Add("AccessToken=@AccessToken");
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
whereClauses.Add("DeviceId=@DeviceId");
}
if (!query.UserId.Equals(Guid.Empty))
{
whereClauses.Add("UserId=@UserId");
}
if (query.HasUser.HasValue)
{
if (query.HasUser.Value)
{
whereClauses.Add("UserId not null");
}
else
{
whereClauses.Add("UserId is null");
}
}
var whereTextWithoutPaging = whereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", whereClauses.ToArray());
commandText += whereTextWithoutPaging;
commandText += " ORDER BY DateLastActivity desc";
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
var offset = query.StartIndex ?? 0;
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
}
}
var statementTexts = new[]
{
commandText,
"select count (Id) from Tokens" + whereTextWithoutPaging
};
var list = new List<AuthenticationInfo>();
var result = new QueryResult<AuthenticationInfo>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
BindAuthenticationQueryParams(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
using (var totalCountStatement = statements[1])
{
BindAuthenticationQueryParams(query, totalCountStatement);
result.TotalRecordCount = totalCountStatement.ExecuteQuery()
.SelectScalarInt()
.First();
}
}
},
ReadTransactionMode);
}
result.Items = list;
return result;
}
private static AuthenticationInfo Get(IReadOnlyList<ResultSetValue> reader)
{
var info = new AuthenticationInfo
{
Id = reader[0].ToInt64(),
AccessToken = reader[1].ToString()
};
if (reader.TryGetString(2, out var deviceId))
{
info.DeviceId = deviceId;
}
if (reader.TryGetString(3, out var appName))
{
info.AppName = appName;
}
if (reader.TryGetString(4, out var appVersion))
{
info.AppVersion = appVersion;
}
if (reader.TryGetString(6, out var userId))
{
info.UserId = new Guid(userId);
}
if (reader.TryGetString(7, out var userName))
{
info.UserName = userName;
}
info.DateCreated = reader[8].ReadDateTime();
if (reader.TryReadDateTime(9, out var dateLastActivity))
{
info.DateLastActivity = dateLastActivity;
}
else
{
info.DateLastActivity = info.DateCreated;
}
if (reader.TryGetString(10, out var customName))
{
info.DeviceName = customName;
}
else if (reader.TryGetString(5, out var deviceName))
{
info.DeviceName = deviceName;
}
return info;
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
using (var connection = GetConnection(true))
{
return connection.RunInTransaction(
db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
statement.TryBind("@DeviceId", deviceId);
var result = new DeviceOptions();
foreach (var row in statement.ExecuteQuery())
{
if (row.TryGetString(0, out var customName))
{
result.CustomName = customName;
}
}
return result;
}
}, ReadTransactionMode);
}
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
statement.TryBind("@Id", deviceId);
if (string.IsNullOrWhiteSpace(options.CustomName))
{
statement.TryBindNull("@CustomName");
}
else
{
statement.TryBind("@CustomName", options.CustomName);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
}
}

@ -10,8 +10,10 @@ using System.Linq;
using System.Threading;
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 Jellyfin.Extensions;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@ -25,9 +27,7 @@ using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library;
@ -55,7 +55,6 @@ namespace Emby.Server.Implementations.Session
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
/// <summary>
@ -78,7 +77,6 @@ namespace Emby.Server.Implementations.Session
IDtoService dtoService,
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
@ -91,7 +89,6 @@ namespace Emby.Server.Implementations.Session
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_appHost = appHost;
_authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
@ -257,7 +254,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
public SessionInfo LogSessionActivity(
public async Task<SessionInfo> LogSessionActivity(
string appName,
string appVersion,
string deviceId,
@ -283,7 +280,7 @@ namespace Emby.Server.Implementations.Session
}
var activityDate = DateTime.UtcNow;
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
@ -296,7 +293,7 @@ namespace Emby.Server.Implementations.Session
try
{
user.LastActivityDate = activityDate;
_userManager.UpdateUser(user);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException e)
{
@ -461,7 +458,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
private SessionInfo GetSessionInfo(
private async Task<SessionInfo> GetSessionInfo(
string appName,
string appVersion,
string deviceId,
@ -480,9 +477,11 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
var sessionInfo = _activeConnections.GetOrAdd(
key,
k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{
_activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
sessionInfo = _activeConnections[key];
}
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = user?.Username;
@ -505,7 +504,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
private SessionInfo CreateSession(
private async Task<SessionInfo> CreateSession(
string key,
string appName,
string appVersion,
@ -535,7 +534,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@ -1433,38 +1432,20 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Authenticates the new session.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Task{SessionInfo}.</returns>
/// <param name="request">The authenticationrequest.</param>
/// <returns>The authentication result.</returns>
public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
{
return AuthenticateNewSessionInternal(request, true);
}
public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request)
{
return AuthenticateNewSessionInternal(request, false);
}
public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
/// <summary>
/// Directly authenticates the session without enforcing password.
/// </summary>
/// <param name="request">The authentication request.</param>
/// <returns>The authentication result.</returns>
public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
{
var result = _authRepo.Get(new AuthenticationInfoQuery()
{
AccessToken = token,
DeviceId = _appHost.SystemId,
Limit = 1
});
if (result.TotalRecordCount == 0)
{
throw new SecurityException("Unknown quick connect token");
}
var info = result.Items[0];
request.UserId = info.UserId;
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
_authRepo.Delete(info);
return AuthenticateNewSessionInternal(request, false);
}
@ -1510,15 +1491,15 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is at their maximum number of sessions.");
}
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
var session = LogSessionActivity(
var session = await LogSessionActivity(
request.App,
request.AppVersion,
request.DeviceId,
request.DeviceName,
request.RemoteEndPoint,
user);
user).ConfigureAwait(false);
var returnResult = new AuthenticationResult
{
@ -1533,36 +1514,33 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
var existing = _authRepo.Get(
new AuthenticationInfoQuery
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id,
Limit = 1
}).Items.FirstOrDefault();
}).ConfigureAwait(false)).Items.FirstOrDefault();
if (!string.IsNullOrEmpty(deviceId))
{
var allExistingForDevice = _authRepo.Get(
new AuthenticationInfoQuery
{
DeviceId = deviceId
}).Items;
var allExistingForDevice = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId
}).ConfigureAwait(false)).Items;
foreach (var auth in allExistingForDevice)
foreach (var auth in allExistingForDevice)
{
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
try
{
try
{
Logout(auth);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while logging out.");
}
await Logout(auth).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while logging out.");
}
}
}
@ -1573,29 +1551,14 @@ namespace Emby.Server.Implementations.Session
return existing.AccessToken;
}
var now = DateTime.UtcNow;
var newToken = new AuthenticationInfo
{
AppName = app,
AppVersion = appVersion,
DateCreated = now,
DateLastActivity = now,
DeviceId = deviceId,
DeviceName = deviceName,
UserId = user.Id,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
UserName = user.Username
};
_logger.LogInformation("Creating new access token for user {0}", user.Id);
_authRepo.Create(newToken);
var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
return newToken.AccessToken;
return device.AccessToken;
}
/// <inheritdoc />
public void Logout(string accessToken)
public async Task Logout(string accessToken)
{
CheckDisposed();
@ -1604,27 +1567,27 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(accessToken));
}
var existing = _authRepo.Get(
new AuthenticationInfoQuery
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
}).Items;
}).ConfigureAwait(false)).Items;
if (existing.Count > 0)
{
Logout(existing[0]);
await Logout(existing[0]).ConfigureAwait(false);
}
}
/// <inheritdoc />
public void Logout(AuthenticationInfo existing)
public async Task Logout(Device existing)
{
CheckDisposed();
_logger.LogInformation("Logging out access token {0}", existing.AccessToken);
_authRepo.Delete(existing);
await _deviceManager.DeleteDevice(existing).ConfigureAwait(false);
var sessions = Sessions
.Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
@ -1644,30 +1607,24 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public void RevokeUserTokens(Guid userId, string currentAccessToken)
public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
{
CheckDisposed();
var existing = _authRepo.Get(new AuthenticationInfoQuery
var existing = await _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
});
}).ConfigureAwait(false);
foreach (var info in existing.Items)
{
if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
{
Logout(info);
await Logout(info).ConfigureAwait(false);
}
}
}
/// <inheritdoc />
public void RevokeToken(string token)
{
Logout(token);
}
/// <summary>
/// Reports the capabilities.
/// </summary>
@ -1787,18 +1744,9 @@ namespace Emby.Server.Implementations.Session
}
var item = _libraryManager.GetItemById(new Guid(itemId));
var info = GetItemInfo(item, null);
ReportNowViewingItem(sessionId, info);
}
/// <inheritdoc />
public void ReportNowViewingItem(string sessionId, BaseItemDto item)
{
var session = GetSession(sessionId);
session.NowViewingItem = item;
session.NowViewingItem = GetItemInfo(item, null);
}
/// <inheritdoc />
@ -1828,7 +1776,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion)
{
if (info == null)
{
@ -1861,20 +1809,20 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
var items = _authRepo.Get(new AuthenticationInfoQuery
var items = (await _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
}).Items;
}).ConfigureAwait(false)).Items;
if (items.Count == 0)
{
return null;
}
return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
}
/// <inheritdoc />

@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection)
{
var session = GetSession(connection.QueryString, connection.RemoteEndPoint.ToString());
var session = await GetSession(connection.QueryString, connection.RemoteEndPoint.ToString()).ConfigureAwait(false);
if (session != null)
{
EnsureController(session, connection);
@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.Session
}
}
private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint)
private Task<SessionInfo> GetSession(IQueryCollection queryString, string remoteEndpoint)
{
if (queryString == null)
{

@ -40,11 +40,11 @@ namespace Jellyfin.Api.Auth
}
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try
{
var authorizationInfo = _authService.Authenticate(Request);
var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false);
var role = UserRoles.User;
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
@ -68,16 +68,16 @@ namespace Jellyfin.Api.Auth
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
return AuthenticateResult.Success(ticket);
}
catch (AuthenticationException ex)
{
_logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler));
return Task.FromResult(AuthenticateResult.NoResult());
return AuthenticateResult.NoResult();
}
catch (SecurityException ex)
{
return Task.FromResult(AuthenticateResult.Fail(ex));
return AuthenticateResult.Fail(ex);
}
}
}

@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
{
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
StartIndex = startIndex,
Skip = startIndex,
Limit = limit,
MinDate = minDate,
HasUserId = hasUserId

@ -1,10 +1,8 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -18,24 +16,15 @@ namespace Jellyfin.Api.Controllers
[Route("Auth")]
public class ApiKeyController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authRepo;
private readonly IAuthenticationManager _authenticationManager;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
/// </summary>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
public ApiKeyController(
ISessionManager sessionManager,
IServerApplicationHost appHost,
IAuthenticationRepository authRepo)
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
public ApiKeyController(IAuthenticationManager authenticationManager)
{
_sessionManager = sessionManager;
_appHost = appHost;
_authRepo = authRepo;
_authenticationManager = authenticationManager;
}
/// <summary>
@ -46,14 +35,15 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
{
var result = _authRepo.Get(new AuthenticationInfoQuery
{
HasUser = false
});
var keys = await _authenticationManager.GetApiKeys();
return result;
return new QueryResult<AuthenticationInfo>
{
Items = keys,
TotalRecordCount = keys.Count
};
}
/// <summary>
@ -65,17 +55,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateKey([FromQuery, Required] string app)
public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
{
_authRepo.Create(new AuthenticationInfo
{
AppName = app,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString
});
await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
return NoContent();
}
@ -88,9 +71,10 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Keys/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RevokeKey([FromRoute, Required] string key)
public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
{
_sessionManager.RevokeToken(key);
await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
return NoContent();
}
}

@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId;
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{

@ -1,8 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
@ -19,22 +22,18 @@ namespace Jellyfin.Api.Controllers
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
private readonly IAuthenticationRepository _authenticationRepository;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="DevicesController"/> class.
/// </summary>
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
/// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
public DevicesController(
IDeviceManager deviceManager,
IAuthenticationRepository authenticationRepository,
ISessionManager sessionManager)
{
_deviceManager = deviceManager;
_authenticationRepository = authenticationRepository;
_sessionManager = sessionManager;
}
@ -47,10 +46,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
return _deviceManager.GetDevices(deviceQuery);
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
}
/// <summary>
@ -63,9 +61,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDevice(id);
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
if (deviceInfo == null)
{
return NotFound();
@ -84,9 +82,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
if (deviceInfo == null)
{
return NotFound();
@ -101,22 +99,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">Device Id.</param>
/// <param name="deviceOptions">Device Options.</param>
/// <response code="204">Device options updated.</response>
/// <response code="404">Device not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Options")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
public async Task<ActionResult> UpdateDeviceOptions(
[FromQuery, Required] string id,
[FromBody, Required] DeviceOptions deviceOptions)
[FromBody, Required] DeviceOptionsDto deviceOptions)
{
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
if (existingDeviceOptions == null)
{
return NotFound();
}
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
return NoContent();
}
@ -130,19 +120,19 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteDevice([FromQuery, Required] string id)
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
if (existingDevice == null)
{
return NotFound();
}
var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
foreach (var session in sessions)
foreach (var session in sessions.Items)
{
_sessionManager.Logout(session);
await _sessionManager.Logout(session).ConfigureAwait(false);
}
return NoContent();

@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}

@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItem(Guid itemId)
public async Task<ActionResult> DeleteItem(Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
if (ids.Length == 0)
{
@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;

@ -429,10 +429,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.DefaultAuthorization)]
public ActionResult ResetTuner([FromRoute, Required] string tunerId)
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{
AssertUserCanManageLiveTv();
_liveTvManager.ResetTuner(tunerId, CancellationToken.None);
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -761,9 +761,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
var item = _libraryManager.GetItemById(recordingId);
if (item == null)
@ -790,7 +790,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@ -808,7 +808,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -824,7 +824,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -882,7 +882,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@ -900,7 +900,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -916,7 +916,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
AssertUserCanManageLiveTv();
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -1212,9 +1212,9 @@ namespace Jellyfin.Api.Controllers
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
private void AssertUserCanManageLiveTv()
private async Task AssertUserCanManageLiveTv()
{
var user = _sessionContext.GetUser(Request);
var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
if (user == null)
{

@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var profile = playbackInfoDto?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);

@ -72,13 +72,13 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkPlayedItem(
public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@ -98,10 +98,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var dto = UpdatePlayedStatus(user, itemId, false, null);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
{
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
{
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
@ -171,10 +171,11 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
@ -220,7 +221,7 @@ namespace Jellyfin.Api.Controllers
};
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
@ -320,10 +321,11 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}

@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Model.QuickConnect;
using Microsoft.AspNetCore.Authorization;
@ -17,14 +19,17 @@ namespace Jellyfin.Api.Controllers
public class QuickConnectController : BaseJellyfinApiController
{
private readonly IQuickConnect _quickConnect;
private readonly IAuthorizationContext _authContext;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
/// </summary>
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
public QuickConnectController(IQuickConnect quickConnect)
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
{
_quickConnect = quickConnect;
_authContext = authContext;
}
/// <summary>
@ -47,11 +52,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
[HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QuickConnectResult> Initiate()
public async Task<ActionResult<QuickConnectResult>> Initiate()
{
try
{
return _quickConnect.TryConnect();
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
return _quickConnect.TryConnect(auth);
}
catch (AuthenticationException)
{
@ -96,7 +102,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<bool> Authorize([FromQuery, Required] string code)
public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code)
{
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
@ -106,7 +112,7 @@ namespace Jellyfin.Api.Controllers
try
{
return _quickConnect.AuthorizeRequest(userId.Value, code);
return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
}
catch (AuthenticationException)
{

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Viewing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DisplayContent(
public async Task<ActionResult> DisplayContent(
[FromRoute, Required] string sessionId,
[FromQuery, Required] string itemType,
[FromQuery, Required] string itemId,
@ -137,11 +138,12 @@ namespace Jellyfin.Api.Controllers
ItemType = itemType
};
_sessionManager.SendBrowseCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
await _sessionManager.SendBrowseCommand(
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
command,
CancellationToken.None);
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -162,7 +164,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Playing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Play(
public async Task<ActionResult> Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
@ -183,11 +185,12 @@ namespace Jellyfin.Api.Controllers
StartIndex = startIndex
};
_sessionManager.SendPlayCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
await _sessionManager.SendPlayCommand(
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
playRequest,
CancellationToken.None);
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -204,14 +207,14 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendPlaystateCommand(
public async Task<ActionResult> SendPlaystateCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] PlaystateCommand command,
[FromQuery] long? seekPositionTicks,
[FromQuery] string? controllingUserId)
{
_sessionManager.SendPlaystateCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
await _sessionManager.SendPlaystateCommand(
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
new PlaystateRequest()
{
@ -219,7 +222,8 @@ namespace Jellyfin.Api.Controllers
ControllingUserId = controllingUserId,
SeekPositionTicks = seekPositionTicks,
},
CancellationToken.None);
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -234,18 +238,18 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/System/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand(
public async Task<ActionResult> SendSystemCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
Name = command,
ControllingUserId = currentSession.UserId
};
_sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
return NoContent();
}
@ -260,11 +264,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Command/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand(
public async Task<ActionResult> SendGeneralCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
@ -272,7 +276,8 @@ namespace Jellyfin.Api.Controllers
ControllingUserId = currentSession.UserId
};
_sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -287,11 +292,12 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Command")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendFullGeneralCommand(
public async Task<ActionResult> SendFullGeneralCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] GeneralCommand command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request)
.ConfigureAwait(false);
if (command == null)
{
@ -300,11 +306,12 @@ namespace Jellyfin.Api.Controllers
command.ControllingUserId = currentSession.UserId;
_sessionManager.SendGeneralCommand(
await _sessionManager.SendGeneralCommand(
currentSession.Id,
sessionId,
command,
CancellationToken.None);
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -319,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Message")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
public async Task<ActionResult> SendMessageCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] MessageCommand command)
{
@ -328,7 +335,12 @@ namespace Jellyfin.Api.Controllers
command.Header = "Message from Server";
}
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
await _sessionManager.SendMessageCommand(
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
command,
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
@ -383,7 +395,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Capabilities")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
@ -393,7 +405,7 @@ namespace Jellyfin.Api.Controllers
{
if (string.IsNullOrWhiteSpace(id))
{
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, new ClientCapabilities
@ -417,13 +429,13 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Capabilities/Full")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities(
public async Task<ActionResult> PostFullCapabilities(
[FromQuery] string? id,
[FromBody, Required] ClientCapabilitiesDto capabilities)
{
if (string.IsNullOrWhiteSpace(id))
{
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
@ -441,11 +453,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Viewing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing(
public async Task<ActionResult> ReportViewing(
[FromQuery] string? sessionId,
[FromQuery, Required] string? itemId)
{
string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
@ -459,11 +471,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Logout")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportSessionEnded()
public async Task<ActionResult> ReportSessionEnded()
{
AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_sessionManager.Logout(auth.Token);
await _sessionManager.Logout(auth.Token).ConfigureAwait(false);
return NoContent();
}

@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
long positionTicks = 0;
var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
while (positionTicks < runtime)
{

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SyncPlayDtos;
@ -51,10 +52,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
public ActionResult SyncPlayCreateGroup(
public async Task<ActionResult> SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult SyncPlayJoinGroup(
public async Task<ActionResult> SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -86,9 +87,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayLeaveGroup()
public async Task<ActionResult> SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new LeaveGroupRequest();
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new ListGroupsRequest();
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
}
@ -118,10 +119,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetNewQueue(
public async Task<ActionResult> SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PlayGroupRequest(
requestData.PlayingQueue,
requestData.PlayingItemPosition,
@ -139,10 +140,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetPlaylistItem(
public async Task<ActionResult> SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -157,10 +158,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayRemoveFromPlaylist(
public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -175,10 +176,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayMovePlaylistItem(
public async Task<ActionResult> SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -193,10 +194,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayQueue(
public async Task<ActionResult> SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -210,9 +211,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayUnpause()
public async Task<ActionResult> SyncPlayUnpause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new UnpauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -226,9 +227,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPause()
public async Task<ActionResult> SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -242,9 +243,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayStop()
public async Task<ActionResult> SyncPlayStop()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new StopGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -259,10 +260,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySeek(
public async Task<ActionResult> SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -277,10 +278,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayBuffering(
public async Task<ActionResult> SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new BufferGroupRequest(
requestData.When,
requestData.PositionTicks,
@ -299,10 +300,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayReady(
public async Task<ActionResult> SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new ReadyGroupRequest(
requestData.When,
requestData.PositionTicks,
@ -321,10 +322,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetIgnoreWait(
public async Task<ActionResult> SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -339,10 +340,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayNextItem(
public async Task<ActionResult> SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -357,10 +358,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPreviousItem(
public async Task<ActionResult> SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -375,10 +376,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetRepeatMode(
public async Task<ActionResult> SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -393,10 +394,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetShuffleMode(
public async Task<ActionResult> SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@ -410,10 +411,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPing(
public async Task<ActionResult> SyncPlayPing(
[FromBody, Required] PingRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PingGroupRequest(requestData.Ping);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();

@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);

@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers
private readonly IAuthorizationContext _authContext;
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IQuickConnect _quickConnectManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
public UserController(
IUserManager userManager,
ISessionManager sessionManager,
@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers
IDeviceManager deviceManager,
IAuthorizationContext authContext,
IServerConfigurationManager config,
ILogger<UserController> logger)
ILogger<UserController> logger,
IQuickConnect quickConnectManager)
{
_userManager = userManager;
_sessionManager = sessionManager;
@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers
_authContext = authContext;
_config = config;
_logger = logger;
_quickConnectManager = quickConnectManager;
}
/// <summary>
@ -77,11 +82,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<UserDto>> GetUsers(
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers(
[FromQuery] bool? isHidden,
[FromQuery] bool? isDisabled)
{
var users = Get(isHidden, isDisabled, false, false);
var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false);
return Ok(users);
}
@ -92,15 +97,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
[HttpGet("Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers()
{
// If the startup wizard hasn't been completed then just return all users
if (!_config.Configuration.IsStartupWizardCompleted)
{
return Ok(Get(false, false, false, false));
return Ok(await Get(false, false, false, false).ConfigureAwait(false));
}
return Ok(Get(false, false, true, true));
return Ok(await Get(false, false, true, true).ConfigureAwait(false));
}
/// <summary>
@ -141,7 +146,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
_sessionManager.RevokeUserTokens(user.Id, null);
await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
}
@ -195,7 +200,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
{
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
try
{
@ -228,23 +233,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateWithQuickConnect")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
{
var auth = _authContext.GetAuthorizationInfo(Request);
try
{
var authRequest = new AuthenticationRequest
{
App = auth.Client,
AppVersion = auth.Version,
DeviceId = auth.DeviceId,
DeviceName = auth.Device,
};
return await _sessionManager.AuthenticateQuickConnect(
authRequest,
request.Token).ConfigureAwait(false);
return _quickConnectManager.GetAuthorizedRequest(request.Secret);
}
catch (SecurityException e)
{
@ -271,7 +264,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
@ -303,9 +296,9 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
_sessionManager.RevokeUserTokens(user.Id, currentToken);
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
return NoContent();
@ -325,11 +318,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
public async Task<ActionResult> UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
@ -343,11 +336,11 @@ namespace Jellyfin.Api.Controllers
if (request.ResetPassword)
{
_userManager.ResetEasyPassword(user);
await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
}
else
{
_userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
}
return NoContent();
@ -371,7 +364,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserDto updateUser)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
@ -431,8 +424,8 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
_sessionManager.RevokeUserTokens(user.Id, currentToken);
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
@ -456,7 +449,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
@ -555,7 +548,7 @@ namespace Jellyfin.Api.Controllers
return _userManager.GetUserDto(user);
}
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
@ -571,7 +564,7 @@ namespace Jellyfin.Api.Controllers
if (filterByDevice)
{
var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
query.PresetViews = presetViews;
}
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty;
if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
{
query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };

@ -468,7 +468,7 @@ namespace Jellyfin.Api.Helpers
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
{
var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@ -59,9 +60,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="userId">The user id.</param>
/// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
/// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
internal static async Task<bool> AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
{
var auth = authContext.GetAuthorizationInfo(requestContext);
var auth = await authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var authenticatedUser = auth.User;
@ -75,17 +76,17 @@ namespace Jellyfin.Api.Helpers
return true;
}
internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
{
var authorization = authContext.GetAuthorizationInfo(request);
var authorization = await authContext.GetAuthorizationInfo(request).ConfigureAwait(false);
var user = authorization.User;
var session = sessionManager.LogSessionActivity(
var session = await sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
request.HttpContext.GetNormalizedRemoteIp().ToString(),
user);
user).ConfigureAwait(false);
if (session == null)
{
@ -95,6 +96,13 @@ namespace Jellyfin.Api.Helpers
return session;
}
internal static async Task<string> GetSessionId(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
{
var session = await GetSession(sessionManager, authContext, request).ConfigureAwait(false);
return session.Id;
}
internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions,

@ -17,9 +17,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@ -101,7 +99,7 @@ namespace Jellyfin.Api.Helpers
EnableDlnaHeaders = enableDlnaHeaders
};
var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
if (!auth.UserId.Equals(Guid.Empty))
{
state.User = userManager.GetUserById(auth.UserId);

@ -495,7 +495,7 @@ namespace Jellyfin.Api.Helpers
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);

@ -8,9 +8,9 @@ namespace Jellyfin.Api.Models.UserDtos
public class QuickConnectDto
{
/// <summary>
/// Gets or sets the quick connect token.
/// Gets or sets the quick connect secret.
/// </summary>
[Required]
public string? Token { get; set; }
public string Secret { get; set; } = null!;
}
}

@ -0,0 +1,23 @@
namespace Jellyfin.Data.Dtos
{
/// <summary>
/// A dto representing custom options for a device.
/// </summary>
public class DeviceOptionsDto
{
/// <summary>
/// Gets or sets the id.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets the custom name.
/// </summary>
public string? CustomName { get; set; }
}
}

@ -0,0 +1,56 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
namespace Jellyfin.Data.Entities.Security
{
/// <summary>
/// An entity representing an API key.
/// </summary>
public class ApiKey
{
/// <summary>
/// Initializes a new instance of the <see cref="ApiKey"/> class.
/// </summary>
/// <param name="name">The name.</param>
public ApiKey(string name)
{
Name = name;
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
DateCreated = DateTime.UtcNow;
}
/// <summary>
/// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; private set; }
/// <summary>
/// Gets or sets the date created.
/// </summary>
public DateTime DateCreated { get; set; }
/// <summary>
/// Gets or sets the date of last activity.
/// </summary>
public DateTime DateLastActivity { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
[MaxLength(64)]
[StringLength(64)]
public string Name { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string AccessToken { get; set; }
}
}

@ -0,0 +1,107 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
namespace Jellyfin.Data.Entities.Security
{
/// <summary>
/// An entity representing a device.
/// </summary>
public class Device
{
/// <summary>
/// Initializes a new instance of the <see cref="Device"/> class.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="appName">The app name.</param>
/// <param name="appVersion">The app version.</param>
/// <param name="deviceName">The device name.</param>
/// <param name="deviceId">The device id.</param>
public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId)
{
UserId = userId;
AppName = appName;
AppVersion = appVersion;
DeviceName = deviceName;
DeviceId = deviceId;
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
DateCreated = DateTime.UtcNow;
DateModified = DateCreated;
DateLastActivity = DateCreated;
// Non-nullable for EF Core, as this is a required relationship.
User = null!;
}
/// <summary>
/// Gets the id.
/// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; private set; }
/// <summary>
/// Gets the user id.
/// </summary>
public Guid UserId { get; private set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets the app name.
/// </summary>
[MaxLength(64)]
[StringLength(64)]
public string AppName { get; set; }
/// <summary>
/// Gets or sets the app version.
/// </summary>
[MaxLength(32)]
[StringLength(32)]
public string AppVersion { get; set; }
/// <summary>
/// Gets or sets the device name.
/// </summary>
[MaxLength(64)]
[StringLength(64)]
public string DeviceName { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
[MaxLength(256)]
[StringLength(256)]
public string DeviceId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this device is active.
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Gets or sets the date created.
/// </summary>
public DateTime DateCreated { get; set; }
/// <summary>
/// Gets or sets the date modified.
/// </summary>
public DateTime DateModified { get; set; }
/// <summary>
/// Gets or sets the date of last activity.
/// </summary>
public DateTime DateLastActivity { get; set; }
/// <summary>
/// Gets the user.
/// </summary>
public User User { get; private set; }
}
}

@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities.Security
{
/// <summary>
/// An entity representing custom options for a device.
/// </summary>
public class DeviceOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="DeviceOptions"/> class.
/// </summary>
/// <param name="deviceId">The device id.</param>
public DeviceOptions(string deviceId)
{
DeviceId = deviceId;
}
/// <summary>
/// Gets the id.
/// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; private set; }
/// <summary>
/// Gets the device id.
/// </summary>
public string DeviceId { get; private set; }
/// <summary>
/// Gets or sets the custom name.
/// </summary>
public string? CustomName { get; set; }
}
}

@ -5,18 +5,8 @@ namespace Jellyfin.Data.Queries
/// <summary>
/// A class representing a query to the activity logs.
/// </summary>
public class ActivityLogQuery
public class ActivityLogQuery : PaginatedQuery
{
/// <summary>
/// Gets or sets the index to start at.
/// </summary>
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the maximum number of items to include.
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to take entries with a user id.
/// </summary>

@ -0,0 +1,25 @@
using System;
namespace Jellyfin.Data.Queries
{
/// <summary>
/// A query to retrieve devices.
/// </summary>
public class DeviceQuery : PaginatedQuery
{
/// <summary>
/// Gets or sets the user id of the device.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string? AccessToken { get; set; }
}
}

@ -0,0 +1,18 @@
namespace Jellyfin.Data.Queries
{
/// <summary>
/// An abstract class for paginated queries.
/// </summary>
public abstract class PaginatedQuery
{
/// <summary>
/// Gets or sets the index to start at.
/// </summary>
public int? Skip { get; set; }
/// <summary>
/// Gets or sets the maximum number of items to include.
/// </summary>
public int? Limit { get; set; }
}
}

@ -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)

@ -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();

@ -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);
}
}
}

@ -2,41 +2,42 @@
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 MediaBrowser.Controller.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
private readonly IAuthenticationRepository _authRepo;
private readonly JellyfinDbProvider _jellyfinDbProvider;
private readonly IUserManager _userManager;
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
{
_authRepo = authRepo;
_jellyfinDbProvider = jellyfinDb;
_userManager = userManager;
}
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
{
return (AuthorizationInfo)cached!; // Cache should never contain null
return Task.FromResult((AuthorizationInfo)cached!); // Cache should never contain null
}
return GetAuthorization(requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
return authInfo;
}
@ -45,22 +46,22 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string>? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
IReadOnlyDictionary<string, string>? auth,
IHeaderDictionary headers,
IQueryCollection queryString)
{
string? deviceId = null;
string? device = null;
string? deviceName = null;
string? client = null;
string? version = null;
string? token = null;
@ -68,7 +69,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (auth != null)
{
auth.TryGetValue("DeviceId", out deviceId);
auth.TryGetValue("Device", out device);
auth.TryGetValue("Device", out deviceName);
auth.TryGetValue("Client", out client);
auth.TryGetValue("Version", out version);
auth.TryGetValue("Token", out token);
@ -99,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
Device = deviceName,
DeviceId = deviceId,
Version = version,
Token = token,
@ -115,88 +116,80 @@ namespace Emby.Server.Implementations.HttpServer.Security
#pragma warning restore CA1508
authInfo.HasToken = true;
var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
await using var dbContext = _jellyfinDbProvider.CreateContext();
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
if (result.Items.Count > 0)
if (device != null)
{
authInfo.IsAuthenticated = true;
}
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = originalAuthenticationInfo.AppName;
authInfo.Client = device.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
authInfo.DeviceId = originalAuthenticationInfo.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 == null || !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
authInfo.Device = originalAuthenticationInfo.DeviceName;
authInfo.Device = device.DeviceName;
}
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
device.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = originalAuthenticationInfo.AppVersion;
authInfo.Version = device.AppVersion;
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
device.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
{
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
device.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
authInfo.User = _userManager.GetUserById(device.UserId);
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
authInfo.IsApiKey = false;
}
else
if (updateToken)
{
authInfo.IsApiKey = true;
dbContext.Devices.Update(device);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
if (updateToken)
}
else
{
var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
if (key != null)
{
_authRepo.Update(originalAuthenticationInfo);
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;
}
}
@ -208,7 +201,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@ -217,7 +210,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
}
/// <summary>
@ -225,7 +218,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@ -234,7 +227,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
}
/// <summary>
@ -242,13 +235,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
var firstSpace = authorizationHeader.IndexOf(' ');
// There should be at least two parts

@ -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));
}

@ -9,14 +9,18 @@ using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@ -84,6 +88,7 @@ namespace Jellyfin.Server
ServiceCollection.AddSingleton<IActivityManager, ActivityManager>();
ServiceCollection.AddSingleton<IUserManager, UserManager>();
ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
// TODO search the assemblies instead of adding them manually?
ServiceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
@ -91,6 +96,10 @@ namespace Jellyfin.Server
ServiceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
ServiceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
base.RegisterServices();
}

@ -25,7 +25,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb),
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.AddPeopleQueryIndex)
typeof(Routines.AddPeopleQueryIndex),
typeof(Routines.MigrateAuthenticationDb)
};
/// <summary>

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.IO;
using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// A migration that moves data from the authentication database into the new schema.
/// </summary>
public class MigrateAuthenticationDb : IMigrationRoutine
{
private const string DbFilename = "authentication.db";
private readonly ILogger<MigrateAuthenticationDb> _logger;
private readonly JellyfinDbProvider _dbProvider;
private readonly IServerApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="dbProvider">The database provider.</param>
/// <param name="appPaths">The server application paths.</param>
public MigrateAuthenticationDb(ILogger<MigrateAuthenticationDb> logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths)
{
_logger = logger;
_dbProvider = dbProvider;
_appPaths = appPaths;
}
/// <inheritdoc />
public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22");
/// <inheritdoc />
public string Name => "MigrateAuthenticationDatabase";
/// <inheritdoc />
public bool PerformOnNewInstall => false;
/// <inheritdoc />
public void Perform()
{
var dataPath = _appPaths.DataPath;
using (var connection = SQLite3.Open(
Path.Combine(dataPath, DbFilename),
ConnectionFlags.ReadOnly,
null))
{
using var dbContext = _dbProvider.CreateContext();
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
foreach (var row in authenticatedDevices)
{
if (row[6].IsDbNull())
{
dbContext.ApiKeys.Add(new ApiKey(row[3].ToString())
{
AccessToken = row[1].ToString(),
DateCreated = row[9].ToDateTime(),
DateLastActivity = row[10].ToDateTime()
});
}
else
{
dbContext.Devices.Add(new Device(
new Guid(row[6].ToString()),
row[3].ToString(),
row[4].ToString(),
row[5].ToString(),
row[2].ToString())
{
AccessToken = row[1].ToString(),
IsActive = row[8].ToBool(),
DateCreated = row[9].ToDateTime(),
DateLastActivity = row[10].ToDateTime()
});
}
}
var deviceOptions = connection.Query("SELECT * FROM Devices");
var deviceIds = new HashSet<string>();
foreach (var row in deviceOptions)
{
if (row[2].IsDbNull())
{
continue;
}
var deviceId = row[2].ToString();
if (deviceIds.Contains(deviceId))
{
continue;
}
deviceIds.Add(deviceId);
dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
{
CustomName = row[1].IsDbNull() ? null : row[1].ToString()
});
}
dbContext.SaveChanges();
}
try
{
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 activity log database to 'authentication.db.old'");
}
}
}
}

@ -3,8 +3,11 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
@ -15,33 +18,52 @@ namespace MediaBrowser.Controller.Devices
{
event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
/// <summary>
/// Creates a new device.
/// </summary>
/// <param name="device">The device to create.</param>
/// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
Task<Device> CreateDevice(Device device);
/// <summary>
/// Saves the capabilities.
/// </summary>
/// <param name="reportedId">The reported identifier.</param>
/// <param name="deviceId">The device id.</param>
/// <param name="capabilities">The capabilities.</param>
void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
/// <summary>
/// Gets the capabilities.
/// </summary>
/// <param name="reportedId">The reported identifier.</param>
/// <param name="deviceId">The device id.</param>
/// <returns>ClientCapabilities.</returns>
ClientCapabilities GetCapabilities(string reportedId);
ClientCapabilities GetCapabilities(string deviceId);
/// <summary>
/// Gets the device information.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceInfo.</returns>
DeviceInfo GetDevice(string id);
Task<DeviceInfo> GetDevice(string id);
/// <summary>
/// Gets devices based on the provided query.
/// </summary>
/// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
/// <summary>
/// Gets the devices.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="userId">The user's id, or <c>null</c>.</param>
/// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync);
Task DeleteDevice(Device device);
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
@ -51,8 +73,8 @@ namespace MediaBrowser.Controller.Devices
/// <returns>Whether the user can access the device.</returns>
bool CanAccessDevice(User user, string deviceId);
void UpdateDeviceOptions(string deviceId, DeviceOptions options);
Task UpdateDeviceOptions(string deviceId, string deviceName);
DeviceOptions GetDeviceOptions(string deviceId);
Task<DeviceOptions> GetDeviceOptions(string deviceId);
}
}

@ -66,14 +66,6 @@ namespace MediaBrowser.Controller.Library
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
/// <summary>
/// Updates the user.
/// </summary>
/// <param name="user">The user.</param>
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
void UpdateUser(User user);
/// <summary>
/// Updates the user.
/// </summary>
@ -110,7 +102,8 @@ namespace MediaBrowser.Controller.Library
/// Resets the easy password.
/// </summary>
/// <param name="user">The user.</param>
void ResetEasyPassword(User user);
/// <returns>Task.</returns>
Task ResetEasyPassword(User user);
/// <summary>
/// Changes the password.
@ -126,7 +119,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="newPassword">New password to use.</param>
/// <param name="newPasswordSha1">Hash of new password.</param>
void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
/// <returns>Task.</returns>
Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
/// <summary>
/// Gets the user dto.
@ -169,7 +163,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// This method updates the user's configuration.
/// This is only included as a stopgap until the new API, using this internally is not recommended.
/// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
/// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>.
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="config">The request containing the new user configuration.</param>
@ -179,7 +173,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// This method updates the user's policy.
/// This is only included as a stopgap until the new API, using this internally is not recommended.
/// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
/// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>.
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="policy">The request containing the new user policy.</param>

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Authorization information. Null if unauthenticated.</returns>
AuthorizationInfo Authenticate(HttpRequest request);
Task<AuthorizationInfo> Authenticate(HttpRequest request);
}
}

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@ -11,14 +12,14 @@ namespace MediaBrowser.Controller.Net
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
/// <returns>A task containing the authorization info.</returns>
Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext);
/// <summary>
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
/// <returns>A <see cref="Task"/> containing the authorization info.</returns>
Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext);
}
}

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
using Microsoft.AspNetCore.Http;
@ -8,12 +9,12 @@ namespace MediaBrowser.Controller.Net
{
public interface ISessionContext
{
SessionInfo GetSession(object requestContext);
Task<SessionInfo> GetSession(object requestContext);
User? GetUser(object requestContext);
Task<User?> GetUser(object requestContext);
SessionInfo GetSession(HttpContext requestContext);
Task<SessionInfo> GetSession(HttpContext requestContext);
User? GetUser(HttpContext requestContext);
Task<User?> GetUser(HttpContext requestContext);
}
}

@ -1,4 +1,7 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.QuickConnect;
namespace MediaBrowser.Controller.QuickConnect
@ -16,8 +19,9 @@ namespace MediaBrowser.Controller.QuickConnect
/// <summary>
/// Initiates a new quick connect request.
/// </summary>
/// <param name="authorizationInfo">The initiator authorization info.</param>
/// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
QuickConnectResult TryConnect();
QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo);
/// <summary>
/// Checks the status of an individual request.
@ -32,6 +36,13 @@ namespace MediaBrowser.Controller.QuickConnect
/// <param name="userId">User id.</param>
/// <param name="code">Identifying code for the request.</param>
/// <returns>A boolean indicating if the authorization completed successfully.</returns>
bool AuthorizeRequest(Guid userId, string code);
Task<bool> AuthorizeRequest(Guid userId, string code);
/// <summary>
/// Gets the authorized request for the secret.
/// </summary>
/// <param name="secret">The secret.</param>
/// <returns>The authentication result.</returns>
AuthenticationResult GetAuthorizedRequest(string secret);
}
}

@ -1,53 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Controller.Security
{
public class AuthenticationInfoQuery
{
/// <summary>
/// Gets or sets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
public string DeviceId { get; set; }
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
/// <value>The user identifier.</value>
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
/// <value>The access token.</value>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is active.
/// </summary>
/// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value>
public bool? IsActive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has user.
/// </summary>
/// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value>
public bool? HasUser { get; set; }
/// <summary>
/// Gets or sets the start index.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the limit.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
}
}

@ -0,0 +1,34 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Security
{
/// <summary>
/// Handles the retrieval and storage of API keys.
/// </summary>
public interface IAuthenticationManager
{
/// <summary>
/// Creates an API key.
/// </summary>
/// <param name="name">The name of the key.</param>
/// <returns>A task representing the creation of the key.</returns>
Task CreateApiKey(string name);
/// <summary>
/// Gets the API keys.
/// </summary>
/// <returns>A task representing the retrieval of the API keys.</returns>
Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys();
/// <summary>
/// Deletes an API key with the provided access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
/// <returns>A task representing the deletion of the API key.</returns>
Task DeleteApiKey(string accessToken);
}
}

@ -1,37 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Security
{
public interface IAuthenticationRepository
{
/// <summary>
/// Creates the specified information.
/// </summary>
/// <param name="info">The information.</param>
void Create(AuthenticationInfo info);
/// <summary>
/// Updates the specified information.
/// </summary>
/// <param name="info">The information.</param>
void Update(AuthenticationInfo info);
/// <summary>
/// Gets the specified query.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>QueryResult{AuthenticationInfo}.</returns>
QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query);
void Delete(AuthenticationInfo info);
DeviceOptions GetDeviceOptions(string deviceId);
void UpdateDeviceOptions(string deviceId, DeviceOptions options);
}
}

@ -6,11 +6,12 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@ -83,8 +84,8 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>Session information.</returns>
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
/// <returns>A task containing the session information.</returns>
Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
/// <summary>
/// Used to report that a session controller has connected.
@ -279,13 +280,6 @@ namespace MediaBrowser.Controller.Session
/// <param name="itemId">The item identifier.</param>
void ReportNowViewingItem(string sessionId, string itemId);
/// <summary>
/// Reports the now viewing item.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
/// <param name="item">The item.</param>
void ReportNowViewingItem(string sessionId, BaseItemDto item);
/// <summary>
/// Authenticates the new session.
/// </summary>
@ -293,20 +287,7 @@ namespace MediaBrowser.Controller.Session
/// <returns>Task{SessionInfo}.</returns>
Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
/// <summary>
/// Authenticates a new session with quick connect.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="token">Quick connect access token.</param>
/// <returns>Task{SessionInfo}.</returns>
Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token);
/// <summary>
/// Creates the new session.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Task&lt;AuthenticationResult&gt;.</returns>
Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request);
Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request);
/// <summary>
/// Reports the capabilities.
@ -344,7 +325,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceId">The device identifier.</param>
/// <param name="remoteEndpoint">The remote endpoint.</param>
/// <returns>SessionInfo.</returns>
SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint);
Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint);
/// <summary>
/// Gets the session by authentication token.
@ -354,28 +335,24 @@ namespace MediaBrowser.Controller.Session
/// <param name="remoteEndpoint">The remote endpoint.</param>
/// <param name="appVersion">The application version.</param>
/// <returns>Task&lt;SessionInfo&gt;.</returns>
SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion);
Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion);
/// <summary>
/// Logouts the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
void Logout(string accessToken);
/// <returns>A <see cref="Task"/> representing the log out process.</returns>
Task Logout(string accessToken);
void Logout(AuthenticationInfo accessToken);
Task Logout(Device accessToken);
/// <summary>
/// Revokes the user tokens.
/// </summary>
/// <param name="userId">User ID.</param>
/// <param name="currentAccessToken">Current access token.</param>
void RevokeUserTokens(Guid userId, string currentAccessToken);
/// <summary>
/// Revokes the token.
/// </summary>
/// <param name="id">The identifier.</param>
void RevokeToken(string id);
/// <param name="userId">The user's id.</param>
/// <param name="currentAccessToken">The current access token.</param>
/// <returns>Task.</returns>
Task RevokeUserTokens(Guid userId, string currentAccessToken);
void CloseIfNeeded(SessionInfo session);
}

@ -15,6 +15,11 @@ namespace MediaBrowser.Model.Devices
public string Name { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>

@ -1,9 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Devices
{
public class DeviceOptions
{
public string? CustomName { get; set; }
}
}

@ -1,21 +0,0 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Model.Devices
{
public class DeviceQuery
{
/// <summary>
/// Gets or sets a value indicating whether [supports synchronize].
/// </summary>
/// <value><c>null</c> if [supports synchronize] contains no value, <c>true</c> if [supports synchronize]; otherwise, <c>false</c>.</value>
public bool? SupportsSync { get; set; }
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
/// <value>The user identifier.</value>
public Guid UserId { get; set; }
}
}

@ -13,17 +13,32 @@ namespace MediaBrowser.Model.QuickConnect
/// <param name="secret">The secret used to query the request state.</param>
/// <param name="code">The code used to allow the request.</param>
/// <param name="dateAdded">The time when the request was created.</param>
public QuickConnectResult(string secret, string code, DateTime dateAdded)
/// <param name="deviceId">The requesting device id.</param>
/// <param name="deviceName">The requesting device name.</param>
/// <param name="appName">The requesting app name.</param>
/// <param name="appVersion">The requesting app version.</param>
public QuickConnectResult(
string secret,
string code,
DateTime dateAdded,
string deviceId,
string deviceName,
string appName,
string appVersion)
{
Secret = secret;
Code = code;
DateAdded = dateAdded;
DeviceId = deviceId;
DeviceName = deviceName;
AppName = appName;
AppVersion = appVersion;
}
/// <summary>
/// Gets a value indicating whether this request is authorized.
/// Gets or sets a value indicating whether this request is authorized.
/// </summary>
public bool Authenticated => Authentication != null;
public bool Authenticated { get; set; }
/// <summary>
/// Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
@ -36,9 +51,24 @@ namespace MediaBrowser.Model.QuickConnect
public string Code { get; }
/// <summary>
/// Gets or sets the private access token.
/// Gets the requesting device id.
/// </summary>
public Guid? Authentication { get; set; }
public string DeviceId { get; }
/// <summary>
/// Gets the requesting device name.
/// </summary>
public string DeviceName { get; }
/// <summary>
/// Gets the requesting app name.
/// </summary>
public string AppName { get; }
/// <summary>
/// Gets the requesting app version.
/// </summary>
public string AppVersion { get; }
/// <summary>
/// Gets or sets the DateTime that this request was created.

@ -136,7 +136,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
.Returns(authorizationInfo);
.Returns(Task.FromResult(authorizationInfo));
return authorizationInfo;
}

@ -1,17 +1,28 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.QuickConnect;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.LiveTv
namespace Jellyfin.Server.Implementations.Tests.QuickConnect
{
public class QuickConnectManagerTests
{
private static readonly AuthorizationInfo _quickConnectAuthInfo = new AuthorizationInfo
{
Device = "Device",
DeviceId = "DeviceId",
Client = "Client",
Version = "1.0.0"
};
private readonly Fixture _fixture;
private readonly ServerConfiguration _config;
private readonly QuickConnectManager _quickConnectManager;
@ -27,6 +38,12 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
ConfigureMembers = true
}).Inject(configManager.Object);
// User object contains circular references.
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_quickConnectManager = _fixture.Create<QuickConnectManager>();
}
@ -36,7 +53,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
[Fact]
public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.Throws<AuthenticationException>(_quickConnectManager.TryConnect);
=> Assert.Throws<AuthenticationException>(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo));
[Fact]
public void CheckRequestStatus_QuickConnectUnavailable_ThrowsAuthenticationException()
@ -44,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
[Fact]
public void AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.Throws<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
=> Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
[Fact]
public void IsEnabled_QuickConnectAvailable_True()
@ -57,17 +74,17 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
public void CheckRequestStatus_QuickConnectAvailable_Success()
{
_config.QuickConnectAvailable = true;
var res1 = _quickConnectManager.TryConnect();
var res1 = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
var res2 = _quickConnectManager.CheckRequestStatus(res1.Secret);
Assert.Equal(res1, res2);
}
[Fact]
public void AuthorizeRequest_QuickConnectAvailable_Success()
public async Task AuthorizeRequest_QuickConnectAvailable_Success()
{
_config.QuickConnectAvailable = true;
var res = _quickConnectManager.TryConnect();
Assert.True(_quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code));
var res = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
Assert.True(await _quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code));
}
}
}

Loading…
Cancel
Save