From b6d92a96c167d4f219abaf8d6dffc492496dc9ab Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 23 Apr 2021 22:35:32 +0200 Subject: [PATCH 01/25] Using static files for the login webapp --- Kyoo.WebLogin/{login.html => index.html} | 14 +++++------ Kyoo/Kyoo.csproj | 4 +-- Kyoo/Startup.cs | 1 + Kyoo/Views/AccountApi.cs | 31 ++++-------------------- Kyoo/settings.json | 7 ++++-- 5 files changed, 20 insertions(+), 37 deletions(-) rename Kyoo.WebLogin/{login.html => index.html} (91%) diff --git a/Kyoo.WebLogin/login.html b/Kyoo.WebLogin/index.html similarity index 91% rename from Kyoo.WebLogin/login.html rename to Kyoo.WebLogin/index.html index 0c32da3d..e875aab5 100644 --- a/Kyoo.WebLogin/login.html +++ b/Kyoo.WebLogin/index.html @@ -3,11 +3,11 @@ Kyoo - Login - - - - - + + + + +
@@ -85,6 +85,6 @@
- + - \ No newline at end of file + diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 2d624b9c..0de3d730 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -92,7 +92,7 @@ true - login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) + wwwroot/login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) PreserveNewest true @@ -105,7 +105,7 @@ - + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 8e1485e3..f77ec15e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -199,6 +199,7 @@ namespace Kyoo FileExtensionContentTypeProvider contentTypeProvider = new(); contentTypeProvider.Mappings[".data"] = "application/octet-stream"; + app.UseDefaultFiles(); app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider, diff --git a/Kyoo/Views/AccountApi.cs b/Kyoo/Views/AccountApi.cs index dc02fcda..dbf34c56 100644 --- a/Kyoo/Views/AccountApi.cs +++ b/Kyoo/Views/AccountApi.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; @@ -47,29 +46,8 @@ namespace Kyoo.Api [FromForm(Name = "picture")] public IFormFile Picture { get; set; } } - - [ApiController] - public class AccountUiController : Controller - { - [HttpGet("login")] - public IActionResult Index() - { - return new PhysicalFileResult(Path.GetFullPath("login/login.html"), "text/html"); - } - - [HttpGet("login/{*file}")] - public IActionResult Index(string file) - { - string path = Path.Combine(Path.GetFullPath("login/"), file); - if (!System.IO.File.Exists(path)) - return NotFound(); - FileExtensionContentTypeProvider provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(path, out string contentType)) - contentType = "text/plain"; - return new PhysicalFileResult(path, contentType); - } - } - + + [Route("api/[controller]")] [ApiController] public class AccountController : Controller, IProfileService @@ -100,7 +78,7 @@ namespace Kyoo.Api return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}}); if (!new EmailAddressAttribute().IsValid(user.Email)) return BadRequest(new[] {new {code = "email", description = "Email must be valid."}}); - User account = new User {UserName = user.Username, Email = user.Email}; + User account = new() {UserName = user.Username, Email = user.Email}; IdentityResult result = await _userManager.CreateAsync(account, user.Password); if (!result.Succeeded) return BadRequest(result.Errors); @@ -151,7 +129,7 @@ namespace Kyoo.Api User user = await _userManager.GetUserAsync(context.Subject); if (user != null) { - List claims = new List + List claims = new() { new Claim("email", user.Email), new Claim("username", user.UserName), @@ -207,6 +185,7 @@ namespace Kyoo.Api [HttpGet("default-permissions")] public ActionResult> GetDefaultPermissions() { + return new List(); return _configuration.GetValue("defaultPermissions").Split(","); } } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index bdd2f362..8de53205 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -21,6 +21,11 @@ } }, + "permissions": { + "default": "read,play,write,admin", + "newUser": "read,play,write,admin" + }, + "parallelTasks": "1", "scheduledTasks": { @@ -35,8 +40,6 @@ "providerPath": "providers", "profilePicturePath": "users/", "plugins": "plugins/", - "defaultPermissions": "read,play,write,admin", - "newUserPermissions": "read,play,write,admin", "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", "subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" } From b7294114b9c352341167f12ec0b6972c5ad9146e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 26 Apr 2021 01:07:05 +0200 Subject: [PATCH 02/25] Reworking the task manager & using unity containers --- Kyoo.Common/Controllers/IPlugin.cs | 39 +++ Kyoo.Common/Controllers/ITask.cs | 188 ++++++++++++++ Kyoo.Common/Controllers/ITaskManager.cs | 35 ++- Kyoo.Common/Kyoo.Common.csproj | 7 + .../Models/Attributes/InjectedAttribute.cs | 14 + Kyoo.Common/Models/Plugin.cs | 10 - Kyoo.Common/Models/Task.cs | 20 -- Kyoo.Common/Module.cs | 24 ++ Kyoo.Common/Utility.cs | 4 +- Kyoo/Controllers/TaskManager.cs | 204 +++++++++++---- Kyoo/CoreModule.cs | 26 ++ Kyoo/Kyoo.csproj | 3 +- Kyoo/Program.cs | 8 +- Kyoo/Startup.cs | 6 +- Kyoo/Tasks/CoreTaskHolder.cs | 18 -- Kyoo/Tasks/Crawler.cs | 62 +++-- Kyoo/Tasks/CreateDatabase.cs | 132 +++++----- Kyoo/Tasks/ExtractMetadata.cs | 240 +++++++++--------- Kyoo/Tasks/MetadataProviderLoader.cs | 92 +++---- Kyoo/Tasks/PluginLoader.cs | 74 +++--- Kyoo/Views/LibraryApi.cs | 2 +- Kyoo/Views/TaskAPI.cs | 29 --- Kyoo/Views/TaskApi.cs | 49 ++++ Kyoo/settings.json | 4 +- 24 files changed, 846 insertions(+), 444 deletions(-) create mode 100644 Kyoo.Common/Controllers/IPlugin.cs create mode 100644 Kyoo.Common/Controllers/ITask.cs create mode 100644 Kyoo.Common/Models/Attributes/InjectedAttribute.cs delete mode 100644 Kyoo.Common/Models/Plugin.cs delete mode 100644 Kyoo.Common/Models/Task.cs create mode 100644 Kyoo.Common/Module.cs create mode 100644 Kyoo/CoreModule.cs delete mode 100644 Kyoo/Tasks/CoreTaskHolder.cs delete mode 100644 Kyoo/Views/TaskAPI.cs create mode 100644 Kyoo/Views/TaskApi.cs diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs new file mode 100644 index 00000000..a6949083 --- /dev/null +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -0,0 +1,39 @@ +namespace Kyoo.Controllers +{ + /// + /// A common interface used to discord plugins + /// + public interface IPlugin + { + /// + /// A slug to identify this plugin in queries. + /// + string Slug { get; } + + /// + /// The name of the plugin + /// + string Name { get; } + + /// + /// The description of this plugin. This will be displayed on the "installed plugins" page. + /// + string Description { get; } + + + /// + /// A configure method that will be runned on plugin's startup. + /// + /// + /// You can have use any services as parameter, they will be injected from the service provider + /// You can add managed types or any type you like using the IUnityContainer like so: + /// + /// public static void Configure(IUnityContainer services) + /// { + /// services.AddTask<MyTask>() + /// } + /// + /// + static void Configure() { } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs new file mode 100644 index 00000000..6ac064e9 --- /dev/null +++ b/Kyoo.Common/Controllers/ITask.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Models.Attributes; + +namespace Kyoo.Controllers +{ + /// + /// A single task parameter. This struct contains metadata to display and utility functions to get them in the taks. + /// + /// This struct will be used to generate the swagger documentation of the task. + public record TaskParameter + { + /// + /// The name of this parameter. + /// + public string Name { get; init; } + + /// + /// The description of this parameter. + /// + public string Description { get; init; } + + /// + /// The type of this parameter. + /// + public Type Type { get; init; } + + /// + /// Is this parameter required or can it be ignored? + /// + public bool IsRequired { get; init; } + + /// + /// The default value of this object. + /// + public object DefaultValue { get; init; } + + /// + /// The value of the parameter. + /// + private object Value { get; init; } + + /// + /// Create a new task parameter. + /// + /// The name of the parameter + /// The description of the parameter + /// The type of the parameter. + /// A new task parameter. + public static TaskParameter Create(string name, string description) + { + return new() + { + Name = name, + Description = description, + Type = typeof(T) + }; + } + + /// + /// Create a parameter's value to give to a task. + /// + /// The name of the parameter + /// The value of the parameter. It's type will be used as parameter's type. + /// The type of the parameter + /// A TaskParameter that can be used as value. + public static TaskParameter CreateValue(string name, T value) + { + return new() + { + Name = name, + Type = typeof(T), + Value = value + }; + } + + /// + /// Create a parameter's value for the current parameter. + /// + /// The value to use + /// A new parameter's value for this current parameter + public TaskParameter CreateValue(object value) + { + return this with {Value = value}; + } + + /// + /// Get the value of this parameter. If the value is of the wrong type, it will be converted. + /// + /// The type of this parameter + /// The value of this parameter. + public T As() + { + return (T)Convert.ChangeType(Value, typeof(T)); + } + } + + /// + /// A parameters container implementing an indexer to allow simple usage of parameters. + /// + public class TaskParameters : List + { + /// + /// An indexer that return the parameter with the specified name. + /// + /// The name of the task (case sensitive) + public TaskParameter this[string name] => this.FirstOrDefault(x => x.Name == name); + + + /// + /// Create a new, empty, + /// + public TaskParameters() {} + + /// + /// Create a with an initial parameters content + /// + /// The list of parameters + public TaskParameters(IEnumerable parameters) + { + AddRange(parameters); + } + } + + /// + /// A common interface that tasks should implement. + /// + public interface ITask + { + /// + /// The slug of the task, used to start it. + /// + public string Slug { get; } + + /// + /// The name of the task that will be displayed to the user. + /// + public string Name { get; } + + /// + /// A quick description of what this task will do. + /// + public string Description { get; } + + /// + /// An optional message to display to help the user. + /// + public string HelpMessage { get; } + + /// + /// Should this task be automatically runned at app startup? + /// + public bool RunOnStartup { get; } + + /// + /// The priority of this task. Only used if is true. + /// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order. + /// + public int Priority { get; } + + /// + /// Start this task. + /// + /// The list of parameters. + /// A token to request the task's cancelation. + /// If this task is not cancelled quickly, it might be killed by the runner. + /// + /// Your task can have any service as a public field and use the , + /// they will be set to an available service from the service container before calling this method. + /// + public Task Run(TaskParameters arguments, CancellationToken cancellationToken); + + /// + /// The list of parameters + /// + /// All parameters that this task as. Every one of them will be given to the run function with a value. + public TaskParameters GetParameters(); + + /// + /// If this task is running, return the percentage of completion of this task or null if no percentage can be given. + /// + /// The percentage of completion of the task. + public int? Progress(); + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs index 5c926eeb..7f3b6013 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -1,13 +1,40 @@ +using System; using System.Collections.Generic; using Kyoo.Models; +using Kyoo.Models.Exceptions; namespace Kyoo.Controllers { + /// + /// A service to handle long running tasks. + /// + /// The concurrent number of running tasks is implementation dependent. public interface ITaskManager { - bool StartTask(string taskSlug, string arguments = null); - ITask GetRunningTask(); - void ReloadTask(); - IEnumerable GetAllTasks(); + /// + /// Start a new task (or queue it). + /// + /// The slug of the task to run + /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit. + /// If the number of arguments is invalid or if an argument can't be converted. + /// The task could not be found. + void StartTask(string taskSlug, Dictionary arguments); + + /// + /// Get all currently running tasks + /// + /// A list of currently running tasks. + ICollection GetRunningTasks(); + + /// + /// Get all availables tasks + /// + /// A list of every tasks that this instance know. + ICollection GetAllTasks(); + + /// + /// Reload tasks and run startup tasks. + /// + void ReloadTasks(); } } \ No newline at end of file diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 21d71761..3f4fdbba 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -24,6 +24,13 @@ + + + + + + ..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\5.0.5\Microsoft.Extensions.DependencyInjection.Abstractions.dll + diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs new file mode 100644 index 00000000..af76eb88 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Kyoo.Controllers; + +namespace Kyoo.Models.Attributes +{ + /// + /// An attribute to inform that the service will be injected automatically by a service provider. + /// + /// + /// It should only be used on and will be injected before calling + /// + [AttributeUsage(AttributeTargets.Property)] + public class InjectedAttribute : Attribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Plugin.cs b/Kyoo.Common/Models/Plugin.cs deleted file mode 100644 index 7947d542..00000000 --- a/Kyoo.Common/Models/Plugin.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Kyoo.Models -{ - public interface IPlugin - { - public string Name { get; } - public ICollection Tasks { get; } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Models/Task.cs b/Kyoo.Common/Models/Task.cs deleted file mode 100644 index 76cfbc52..00000000 --- a/Kyoo.Common/Models/Task.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Kyoo.Models -{ - public interface ITask - { - public string Slug { get; } - public string Name { get; } - public string Description { get; } - public string HelpMessage { get; } - public bool RunOnStartup { get; } - public int Priority { get; } - public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null); - public Task> GetPossibleParameters(); - public int? Progress(); - } -} \ No newline at end of file diff --git a/Kyoo.Common/Module.cs b/Kyoo.Common/Module.cs new file mode 100644 index 00000000..41b5859c --- /dev/null +++ b/Kyoo.Common/Module.cs @@ -0,0 +1,24 @@ +using Kyoo.Controllers; +using Unity; + +namespace Kyoo +{ + /// + /// A static class with helper functions to setup external modules + /// + public static class Module + { + /// + /// Register a new task to the container. + /// + /// The container + /// The type of the task + /// The initial container. + public static IUnityContainer AddTask(this IUnityContainer services) + where T : class, ITask + { + services.RegisterSingleton(); + return services; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 220b2003..3e4bba15 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -529,9 +529,9 @@ namespace Kyoo await action(i); } - private static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) + public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) { - MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public | BindingFlags.NonPublic) + MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) .Where(x => x.Name == name) .Where(x => x.GetGenericArguments().Length == generics.Length) .Where(x => x.GetParameters().Length == args.Length) diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 519fb43f..e93c050f 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -1,56 +1,129 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Kyoo.Models; -using Kyoo.Tasks; +using Kyoo.Models.Attributes; +using Kyoo.Models.Exceptions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Unity; namespace Kyoo.Controllers { + /// + /// A service to handle long running tasks and a background runner. + /// + /// Task will be queued, only one can run simultaneously. public class TaskManager : BackgroundService, ITaskManager { - private readonly IServiceProvider _serviceProvider; - private readonly IPluginManager _pluginManager; + /// + /// The service provider used to activate + /// + private readonly IUnityContainer _container; + /// + /// The configuration instance used to get schedule informations + /// private readonly IConfiguration _configuration; - - private List<(ITask task, DateTime scheduledDate)> _tasks = new List<(ITask, DateTime)>(); - private CancellationTokenSource _taskToken = new CancellationTokenSource(); + /// + /// The logger instance. + /// + private readonly ILogger _logger; + + /// + /// The list of tasks and their next scheduled run. + /// + private List<(ITask task, DateTime scheduledDate)> _tasks; + /// + /// The queue of tasks that should be runned as soon as possible. + /// + private readonly Queue<(ITask, Dictionary)> _queuedTasks = new(); + /// + /// The currently running task. + /// private ITask _runningTask; - private Queue<(ITask, string)> _queuedTasks = new Queue<(ITask, string)>(); + /// + /// The cancellation token used to cancel the running task when the runner should shutdown. + /// + private readonly CancellationTokenSource _taskToken = new(); - public TaskManager(IServiceProvider serviceProvider, IPluginManager pluginManager, IConfiguration configuration) + + /// + /// Create a new . + /// + /// The list of tasks to manage + /// The service provider to request services for tasks + /// The configuration to load schedule information. + /// The logger. + public TaskManager(IEnumerable tasks, + IUnityContainer container, + IConfiguration configuration, + ILogger logger) { - _serviceProvider = serviceProvider; - _pluginManager = pluginManager; - _configuration = configuration; + _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + _container = container; + _configuration = configuration.GetSection("scheduledTasks"); + _logger = logger; } + + /// + /// Triggered when the application host is ready to start the service. + /// + /// Start the runner in another thread. + /// Indicates that the start process has been aborted. + public override Task StartAsync(CancellationToken cancellationToken) + { + Task.Run(() => base.StartAsync(cancellationToken), CancellationToken.None); + return Task.CompletedTask; + } + + /// + public override Task StopAsync(CancellationToken cancellationToken) + { + _taskToken.Cancel(); + return base.StopAsync(cancellationToken); + } + + /// + /// The runner that will host tasks and run queued tasks. + /// + /// A token to stop the runner protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - ReloadTask(); - - IEnumerable startupTasks = _tasks.Select(x => x.task) - .Where(x => x.RunOnStartup && x.Priority != Int32.MaxValue) - .OrderByDescending(x => x.Priority); - foreach (ITask task in startupTasks) - _queuedTasks.Enqueue((task, null)); + EnqueueStartupTasks(); while (!cancellationToken.IsCancellationRequested) { if (_queuedTasks.Any()) { - (ITask task, string arguments) = _queuedTasks.Dequeue(); + (ITask task, Dictionary arguments) = _queuedTasks.Dequeue(); _runningTask = task; try { - await task.Run(_serviceProvider, _taskToken.Token, arguments); + ICollection all = task.GetParameters(); + TaskParameters args = new(arguments + .Select(x => (value: x, arg: all + .FirstOrDefault(y => string.Equals(y.Name, x.Key, StringComparison.OrdinalIgnoreCase)))) + .Select(x => + { + if (x.arg == null) + throw new ArgumentException($"Invalid argument name: {x.value.Key}"); + return x.arg.CreateValue(x.value.Value); + })); + + + _logger.LogInformation("Task starting: {Task}", task.Name); + InjectServices(task); + await task.Run(args, _taskToken.Token); + _logger.LogInformation("Task finished: {Task}", task.Name); } catch (Exception e) { - Console.Error.WriteLine($"An unhandled exception occured while running the task {task.Name}.\nInner exception: {e.Message}\n\n"); + _logger.LogError("An unhandled exception occured while running the task {Task}.\n" + + "Inner exception: {Exception}\n\n", task.Name, e.Message); } } else @@ -61,67 +134,90 @@ namespace Kyoo.Controllers } } + /// + /// Inject services into the marked properties of the given object. + /// + /// The object to inject + /// The type of the object. + private void InjectServices(T obj) + { + IEnumerable properties = typeof(T).GetProperties() + .Where(x => x.GetCustomAttribute() != null) + .Where(x => x.CanWrite); + + foreach (PropertyInfo property in properties) + { + object value = _container.Resolve(property.PropertyType); + property.SetValue(obj, value); + } + } + + /// + /// Start tasks that are scheduled for start. + /// private void QueueScheduledTasks() { IEnumerable tasksToQueue = _tasks.Where(x => x.scheduledDate <= DateTime.Now) .Select(x => x.task.Slug); foreach (string task in tasksToQueue) - StartTask(task); + { + _logger.LogDebug("Queuing task scheduled for running: {Task}", task); + StartTask(task, new Dictionary()); + } } - public override Task StartAsync(CancellationToken cancellationToken) + /// + /// Queue startup tasks with respect to the priority rules. + /// + private void EnqueueStartupTasks() { - Task.Run(() => base.StartAsync(cancellationToken)); - return Task.CompletedTask; + IEnumerable startupTasks = _tasks.Select(x => x.task) + .Where(x => x.RunOnStartup && x.Priority != int.MaxValue) + .OrderByDescending(x => x.Priority); + foreach (ITask task in startupTasks) + _queuedTasks.Enqueue((task, null)); } - public override Task StopAsync(CancellationToken cancellationToken) - { - _taskToken.Cancel(); - return base.StopAsync(cancellationToken); - } - - public bool StartTask(string taskSlug, string arguments = null) + /// + public void StartTask(string taskSlug, Dictionary arguments) { int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); if (index == -1) - return false; + throw new ItemNotFound($"No task found with the slug {taskSlug}"); _queuedTasks.Enqueue((_tasks[index].task, arguments)); _tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug)); - return true; } - public TimeSpan GetTaskDelay(string taskSlug) + /// + /// Get the delay of a task + /// + /// The slug of the task + /// The delay of the task. + private TimeSpan GetTaskDelay(string taskSlug) { - TimeSpan delay = _configuration.GetSection("scheduledTasks").GetValue(taskSlug); + TimeSpan delay = _configuration.GetValue(taskSlug); if (delay == default) - delay = TimeSpan.FromDays(365); + delay = TimeSpan.MaxValue; return delay; } - public ITask GetRunningTask() + /// + public ICollection GetRunningTasks() { - return _runningTask; + return new[] {_runningTask}; } - public void ReloadTask() + /// + public ICollection GetAllTasks() { - _tasks.Clear(); - _tasks.AddRange(CoreTaskHolder.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug)))); - - IEnumerable prerunTasks = _tasks.Select(x => x.task) - .Where(x => x.RunOnStartup && x.Priority == int.MaxValue); - - foreach (ITask task in prerunTasks) - task.Run(_serviceProvider, _taskToken.Token); - foreach (IPlugin plugin in _pluginManager.GetAllPlugins()) - if (plugin.Tasks != null) - _tasks.AddRange(plugin.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug)))); + return _tasks.Select(x => x.task).ToArray(); } - public IEnumerable GetAllTasks() + /// + public void ReloadTasks() { - return _tasks.Select(x => x.task); + _tasks = _container.ResolveAll().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + EnqueueStartupTasks(); } } } \ No newline at end of file diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs new file mode 100644 index 00000000..d194a267 --- /dev/null +++ b/Kyoo/CoreModule.cs @@ -0,0 +1,26 @@ +using Kyoo.Controllers; +using Unity; + +namespace Kyoo +{ + /// + /// The core module ccontaining default implementations + /// + public class CoreModule : IPlugin + { + /// + public string Slug => "core"; + + /// + public string Name => "Core"; + + /// + public string Description => "The core module containing default implementations."; + + /// + public static void Configure(IUnityContainer container) + { + container.AddTask(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 2d624b9c..92c27dba 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -33,6 +33,8 @@ + + @@ -56,7 +58,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 1fa2b7c3..ee13bcdb 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -1,13 +1,14 @@ using System; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Unity; +using Unity.Microsoft.DependencyInjection; namespace Kyoo { @@ -39,6 +40,7 @@ namespace Kyoo if (debug == null && Environment.GetEnvironmentVariable("ENVIRONMENT") != null) Console.WriteLine($"Invalid ENVIRONMENT variable. Supported values are \"debug\" and \"prod\". Ignoring..."); + #if DEBUG debug ??= true; #endif @@ -70,7 +72,8 @@ namespace Kyoo /// A new web host instance private static IWebHostBuilder CreateWebHostBuilder(string[] args) { - WebHost.CreateDefaultBuilder(args); + UnityContainer container = new(); + container.EnableDebugDiagnostic(); return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) @@ -89,6 +92,7 @@ namespace Kyoo if (context.HostingEnvironment.IsDevelopment()) StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); }) + .UseUnityServiceProvider(container) .ConfigureServices(x => x.AddRouting()) .UseKestrel(options => { options.AddServerHeader = false; }) .UseIIS() diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 8e1485e3..5f259d24 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -20,6 +20,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Unity; namespace Kyoo { @@ -185,7 +186,7 @@ namespace Kyoo services.AddHostedService(provider => (TaskManager)provider.GetService()); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IUnityContainer container) { if (env.IsDevelopment()) { @@ -247,6 +248,9 @@ namespace Kyoo if (env.IsDevelopment()) spa.UseAngularCliServer("start"); }); + + CoreModule.Configure(container); + container.Resolve().ReloadTasks(); } } } diff --git a/Kyoo/Tasks/CoreTaskHolder.cs b/Kyoo/Tasks/CoreTaskHolder.cs deleted file mode 100644 index 867c83f4..00000000 --- a/Kyoo/Tasks/CoreTaskHolder.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Kyoo.Controllers; -using Kyoo.Models; - -namespace Kyoo.Tasks -{ - public static class CoreTaskHolder - { - public static readonly ITask[] Tasks = - { - new CreateDatabase(), - new PluginLoader(), - new Crawler(), - new MetadataProviderLoader(), - // new ReScan(), - new ExtractMetadata() - }; - } -} \ No newline at end of file diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 47c931c9..d86f54d9 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; using Microsoft.Extensions.DependencyInjection; @@ -21,19 +22,20 @@ namespace Kyoo.Controllers public bool RunOnStartup => true; public int Priority => 0; - private IServiceProvider _serviceProvider; - private IThumbnailsManager _thumbnailsManager; - private IProviderManager _metadataProvider; - private ITranscoder _transcoder; - private IConfiguration _config; + [Injected] public IServiceProvider ServiceProvider { private get; set; } + [Injected] public IThumbnailsManager ThumbnailsManager { private get; set; } + [Injected] public IProviderManager MetadataProvider { private get; set; } + [Injected] public ITranscoder Transcoder { private get; set; } + [Injected] public IConfiguration Config { private get; set; } private int _parallelTasks; - public async Task> GetPossibleParameters() + public TaskParameters GetParameters() { - using IServiceScope serviceScope = _serviceProvider.CreateScope(); - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - return (await libraryManager!.GetAll()).Select(x => x.Slug); + return new() + { + TaskParameter.Create("slug", "A library slug to restrict the scan to this library.") + }; } public int? Progress() @@ -42,20 +44,16 @@ namespace Kyoo.Controllers return null; } - public async Task Run(IServiceProvider serviceProvider, - CancellationToken cancellationToken, - string argument = null) + public async Task Run(TaskParameters parameters, + CancellationToken cancellationToken) { - _serviceProvider = serviceProvider; - _thumbnailsManager = serviceProvider.GetService(); - _metadataProvider = serviceProvider.GetService(); - _transcoder = serviceProvider.GetService(); - _config = serviceProvider.GetService(); - _parallelTasks = _config.GetValue("parallelTasks"); + string argument = parameters["slug"].As(); + + _parallelTasks = Config.GetValue("parallelTasks"); if (_parallelTasks <= 0) _parallelTasks = 30; - using IServiceScope serviceScope = _serviceProvider.CreateScope(); + using IServiceScope serviceScope = ServiceProvider.CreateScope(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); foreach (Show show in await libraryManager!.GetAll()) @@ -148,10 +146,10 @@ namespace Kyoo.Controllers { if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) return; - using IServiceScope serviceScope = _serviceProvider.CreateScope(); + using IServiceScope serviceScope = ServiceProvider.CreateScope(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - string patern = _config.GetValue("subtitleRegex"); + string patern = Config.GetValue("subtitleRegex"); Regex regex = new(patern, RegexOptions.IgnoreCase); Match match = regex.Match(path); @@ -195,10 +193,10 @@ namespace Kyoo.Controllers try { - using IServiceScope serviceScope = _serviceProvider.CreateScope(); + using IServiceScope serviceScope = ServiceProvider.CreateScope(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - string patern = _config.GetValue("regex"); + string patern = Config.GetValue("regex"); Regex regex = new(patern, RegexOptions.IgnoreCase); Match match = regex.Match(relativePath); @@ -257,7 +255,7 @@ namespace Kyoo.Controllers Collection collection = await libraryManager.Get(Utility.ToSlug(collectionName)); if (collection != null) return collection; - collection = await _metadataProvider.GetCollectionFromName(collectionName, library); + collection = await MetadataProvider.GetCollectionFromName(collectionName, library); try { @@ -282,9 +280,9 @@ namespace Kyoo.Controllers await libraryManager.Load(old, x => x.ExternalIDs); return old; } - Show show = await _metadataProvider.SearchShow(showTitle, isMovie, library); + Show show = await MetadataProvider.SearchShow(showTitle, isMovie, library); show.Path = showPath; - show.People = await _metadataProvider.GetPeople(show, library); + show.People = await MetadataProvider.GetPeople(show, library); try { @@ -301,7 +299,7 @@ namespace Kyoo.Controllers show.Slug += $"-{show.StartYear}"; await libraryManager.Create(show); } - await _thumbnailsManager.Validate(show); + await ThumbnailsManager.Validate(show); return show; } @@ -320,9 +318,9 @@ namespace Kyoo.Controllers } catch (ItemNotFound) { - Season season = await _metadataProvider.GetSeason(show, seasonNumber, library); + Season season = await MetadataProvider.GetSeason(show, seasonNumber, library); await libraryManager.CreateIfNotExists(season); - await _thumbnailsManager.Validate(season); + await ThumbnailsManager.Validate(season); season.Show = show; return season; } @@ -336,7 +334,7 @@ namespace Kyoo.Controllers string episodePath, Library library) { - Episode episode = await _metadataProvider.GetEpisode(show, + Episode episode = await MetadataProvider.GetEpisode(show, episodePath, season?.SeasonNumber ?? -1, episodeNumber, @@ -346,7 +344,7 @@ namespace Kyoo.Controllers season ??= await GetSeason(libraryManager, show, episode.SeasonNumber, library); episode.Season = season; episode.SeasonID = season?.ID; - await _thumbnailsManager.Validate(episode); + await ThumbnailsManager.Validate(episode); await GetTracks(episode); return episode; } @@ -367,7 +365,7 @@ namespace Kyoo.Controllers private async Task> GetTracks(Episode episode) { - episode.Tracks = (await _transcoder.ExtractInfos(episode, false)) + episode.Tracks = (await Transcoder.ExtractInfos(episode, false)) .Where(x => x.Type != StreamType.Attachment) .ToArray(); return episode.Tracks; diff --git a/Kyoo/Tasks/CreateDatabase.cs b/Kyoo/Tasks/CreateDatabase.cs index 2264b116..8d54cd87 100644 --- a/Kyoo/Tasks/CreateDatabase.cs +++ b/Kyoo/Tasks/CreateDatabase.cs @@ -1,66 +1,66 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using IdentityServer4.EntityFramework.DbContexts; -using IdentityServer4.EntityFramework.Mappers; -using IdentityServer4.Models; -using Kyoo.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Tasks -{ - public class CreateDatabase : ITask - { - public string Slug => "create-database"; - public string Name => "Create the database"; - public string Description => "Create the database if it does not exit and initialize it with defaults value."; - public string HelpMessage => null; - public bool RunOnStartup => true; - public int Priority => int.MaxValue; - - public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) - { - using IServiceScope serviceScope = serviceProvider.CreateScope(); - DatabaseContext databaseContext = serviceScope.ServiceProvider.GetService(); - IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService(); - ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService(); - - databaseContext!.Database.Migrate(); - identityDatabase!.Database.Migrate(); - identityContext!.Database.Migrate(); - - if (!identityContext.Clients.Any()) - { - foreach (Client client in IdentityContext.GetClients()) - identityContext.Clients.Add(client.ToEntity()); - identityContext.SaveChanges(); - } - if (!identityContext.IdentityResources.Any()) - { - foreach (IdentityResource resource in IdentityContext.GetIdentityResources()) - identityContext.IdentityResources.Add(resource.ToEntity()); - identityContext.SaveChanges(); - } - if (!identityContext.ApiResources.Any()) - { - foreach (ApiResource resource in IdentityContext.GetApis()) - identityContext.ApiResources.Add(resource.ToEntity()); - identityContext.SaveChanges(); - } - return Task.CompletedTask; - } - - public Task> GetPossibleParameters() - { - return Task.FromResult>(null); - } - - public int? Progress() - { - return null; - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; +// using IdentityServer4.EntityFramework.DbContexts; +// using IdentityServer4.EntityFramework.Mappers; +// using IdentityServer4.Models; +// using Kyoo.Models; +// using Microsoft.EntityFrameworkCore; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kyoo.Tasks +// { +// public class CreateDatabase : ITask +// { +// public string Slug => "create-database"; +// public string Name => "Create the database"; +// public string Description => "Create the database if it does not exit and initialize it with defaults value."; +// public string HelpMessage => null; +// public bool RunOnStartup => true; +// public int Priority => int.MaxValue; +// +// public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) +// { +// using IServiceScope serviceScope = serviceProvider.CreateScope(); +// DatabaseContext databaseContext = serviceScope.ServiceProvider.GetService(); +// IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService(); +// ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService(); +// +// databaseContext!.Database.Migrate(); +// identityDatabase!.Database.Migrate(); +// identityContext!.Database.Migrate(); +// +// if (!identityContext.Clients.Any()) +// { +// foreach (Client client in IdentityContext.GetClients()) +// identityContext.Clients.Add(client.ToEntity()); +// identityContext.SaveChanges(); +// } +// if (!identityContext.IdentityResources.Any()) +// { +// foreach (IdentityResource resource in IdentityContext.GetIdentityResources()) +// identityContext.IdentityResources.Add(resource.ToEntity()); +// identityContext.SaveChanges(); +// } +// if (!identityContext.ApiResources.Any()) +// { +// foreach (ApiResource resource in IdentityContext.GetApis()) +// identityContext.ApiResources.Add(resource.ToEntity()); +// identityContext.SaveChanges(); +// } +// return Task.CompletedTask; +// } +// +// public Task> GetPossibleParameters() +// { +// return Task.FromResult>(null); +// } +// +// public int? Progress() +// { +// return null; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo/Tasks/ExtractMetadata.cs b/Kyoo/Tasks/ExtractMetadata.cs index d5513c97..d3e339cb 100644 --- a/Kyoo/Tasks/ExtractMetadata.cs +++ b/Kyoo/Tasks/ExtractMetadata.cs @@ -1,120 +1,120 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Tasks -{ - public class ExtractMetadata : ITask - { - public string Slug => "extract"; - public string Name => "Metadata Extractor"; - public string Description => "Extract subtitles or download thumbnails for a show/episode."; - public string HelpMessage => null; - public bool RunOnStartup => false; - public int Priority => 0; - - - private ILibraryManager _library; - private IThumbnailsManager _thumbnails; - private ITranscoder _transcoder; - - public async Task Run(IServiceProvider serviceProvider, CancellationToken token, string arguments = null) - { - string[] args = arguments?.Split('/'); - - if (args == null || args.Length < 2) - return; - - string slug = args[1]; - bool thumbs = args.Length < 3 || string.Equals(args[2], "thumbnails", StringComparison.InvariantCultureIgnoreCase); - bool subs = args.Length < 3 || string.Equals(args[2], "subs", StringComparison.InvariantCultureIgnoreCase); - - using IServiceScope serviceScope = serviceProvider.CreateScope(); - _library = serviceScope.ServiceProvider.GetService(); - _thumbnails = serviceScope.ServiceProvider.GetService(); - _transcoder = serviceScope.ServiceProvider.GetService(); - int id; - - switch (args[0].ToLowerInvariant()) - { - case "show": - case "shows": - Show show = await (int.TryParse(slug, out id) - ? _library!.Get(id) - : _library!.Get(slug)); - await ExtractShow(show, thumbs, subs, token); - break; - case "season": - case "seasons": - Season season = await (int.TryParse(slug, out id) - ? _library!.Get(id) - : _library!.Get(slug)); - await ExtractSeason(season, thumbs, subs, token); - break; - case "episode": - case "episodes": - Episode episode = await (int.TryParse(slug, out id) - ? _library!.Get(id) - : _library!.Get(slug)); - await ExtractEpisode(episode, thumbs, subs); - break; - } - } - - private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token) - { - if (thumbs) - await _thumbnails!.Validate(show, true); - await _library.Load(show, x => x.Seasons); - foreach (Season season in show.Seasons) - { - if (token.IsCancellationRequested) - return; - await ExtractSeason(season, thumbs, subs, token); - } - } - - private async Task ExtractSeason(Season season, bool thumbs, bool subs, CancellationToken token) - { - if (thumbs) - await _thumbnails!.Validate(season, true); - await _library.Load(season, x => x.Episodes); - foreach (Episode episode in season.Episodes) - { - if (token.IsCancellationRequested) - return; - await ExtractEpisode(episode, thumbs, subs); - } - } - - private async Task ExtractEpisode(Episode episode, bool thumbs, bool subs) - { - if (thumbs) - await _thumbnails!.Validate(episode, true); - if (subs) - { - await _library.Load(episode, x => x.Tracks); - episode.Tracks = (await _transcoder!.ExtractInfos(episode, true)) - .Where(x => x.Type != StreamType.Attachment) - .Concat(episode.Tracks.Where(x => x.IsExternal)) - .ToList(); - await _library.Edit(episode, false); - } - } - - public Task> GetPossibleParameters() - { - return Task.FromResult>(null); - } - - public int? Progress() - { - return null; - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; +// using Kyoo.Controllers; +// using Kyoo.Models; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kyoo.Tasks +// { +// public class ExtractMetadata : ITask +// { +// public string Slug => "extract"; +// public string Name => "Metadata Extractor"; +// public string Description => "Extract subtitles or download thumbnails for a show/episode."; +// public string HelpMessage => null; +// public bool RunOnStartup => false; +// public int Priority => 0; +// +// +// private ILibraryManager _library; +// private IThumbnailsManager _thumbnails; +// private ITranscoder _transcoder; +// +// public async Task Run(IServiceProvider serviceProvider, CancellationToken token, string arguments = null) +// { +// string[] args = arguments?.Split('/'); +// +// if (args == null || args.Length < 2) +// return; +// +// string slug = args[1]; +// bool thumbs = args.Length < 3 || string.Equals(args[2], "thumbnails", StringComparison.InvariantCultureIgnoreCase); +// bool subs = args.Length < 3 || string.Equals(args[2], "subs", StringComparison.InvariantCultureIgnoreCase); +// +// using IServiceScope serviceScope = serviceProvider.CreateScope(); +// _library = serviceScope.ServiceProvider.GetService(); +// _thumbnails = serviceScope.ServiceProvider.GetService(); +// _transcoder = serviceScope.ServiceProvider.GetService(); +// int id; +// +// switch (args[0].ToLowerInvariant()) +// { +// case "show": +// case "shows": +// Show show = await (int.TryParse(slug, out id) +// ? _library!.Get(id) +// : _library!.Get(slug)); +// await ExtractShow(show, thumbs, subs, token); +// break; +// case "season": +// case "seasons": +// Season season = await (int.TryParse(slug, out id) +// ? _library!.Get(id) +// : _library!.Get(slug)); +// await ExtractSeason(season, thumbs, subs, token); +// break; +// case "episode": +// case "episodes": +// Episode episode = await (int.TryParse(slug, out id) +// ? _library!.Get(id) +// : _library!.Get(slug)); +// await ExtractEpisode(episode, thumbs, subs); +// break; +// } +// } +// +// private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token) +// { +// if (thumbs) +// await _thumbnails!.Validate(show, true); +// await _library.Load(show, x => x.Seasons); +// foreach (Season season in show.Seasons) +// { +// if (token.IsCancellationRequested) +// return; +// await ExtractSeason(season, thumbs, subs, token); +// } +// } +// +// private async Task ExtractSeason(Season season, bool thumbs, bool subs, CancellationToken token) +// { +// if (thumbs) +// await _thumbnails!.Validate(season, true); +// await _library.Load(season, x => x.Episodes); +// foreach (Episode episode in season.Episodes) +// { +// if (token.IsCancellationRequested) +// return; +// await ExtractEpisode(episode, thumbs, subs); +// } +// } +// +// private async Task ExtractEpisode(Episode episode, bool thumbs, bool subs) +// { +// if (thumbs) +// await _thumbnails!.Validate(episode, true); +// if (subs) +// { +// await _library.Load(episode, x => x.Tracks); +// episode.Tracks = (await _transcoder!.ExtractInfos(episode, true)) +// .Where(x => x.Type != StreamType.Attachment) +// .Concat(episode.Tracks.Where(x => x.IsExternal)) +// .ToList(); +// await _library.Edit(episode, false); +// } +// } +// +// public Task> GetPossibleParameters() +// { +// return Task.FromResult>(null); +// } +// +// public int? Progress() +// { +// return null; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo/Tasks/MetadataProviderLoader.cs b/Kyoo/Tasks/MetadataProviderLoader.cs index 5811775e..899a2657 100644 --- a/Kyoo/Tasks/MetadataProviderLoader.cs +++ b/Kyoo/Tasks/MetadataProviderLoader.cs @@ -1,46 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Tasks -{ - public class MetadataProviderLoader : ITask - { - public string Slug => "reload-metdata"; - public string Name => "Reload Metadata Providers"; - public string Description => "Add every loaded metadata provider to the database."; - public string HelpMessage => null; - public bool RunOnStartup => true; - public int Priority => 1000; - - public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) - { - using IServiceScope serviceScope = serviceProvider.CreateScope(); - IProviderRepository providers = serviceScope.ServiceProvider.GetService(); - IThumbnailsManager thumbnails = serviceScope.ServiceProvider.GetService(); - IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); - - foreach (IMetadataProvider provider in pluginManager!.GetPlugins()) - { - if (string.IsNullOrEmpty(provider.Provider.Slug)) - throw new ArgumentException($"Empty provider slug (name: {provider.Provider.Name})."); - await providers!.CreateIfNotExists(provider.Provider); - await thumbnails!.Validate(provider.Provider); - } - } - - public Task> GetPossibleParameters() - { - return Task.FromResult>(null); - } - - public int? Progress() - { - return null; - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.Threading; +// using System.Threading.Tasks; +// using Kyoo.Controllers; +// using Kyoo.Models; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kyoo.Tasks +// { +// public class MetadataProviderLoader : ITask +// { +// public string Slug => "reload-metdata"; +// public string Name => "Reload Metadata Providers"; +// public string Description => "Add every loaded metadata provider to the database."; +// public string HelpMessage => null; +// public bool RunOnStartup => true; +// public int Priority => 1000; +// +// public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) +// { +// using IServiceScope serviceScope = serviceProvider.CreateScope(); +// IProviderRepository providers = serviceScope.ServiceProvider.GetService(); +// IThumbnailsManager thumbnails = serviceScope.ServiceProvider.GetService(); +// IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); +// +// foreach (IMetadataProvider provider in pluginManager!.GetPlugins()) +// { +// if (string.IsNullOrEmpty(provider.Provider.Slug)) +// throw new ArgumentException($"Empty provider slug (name: {provider.Provider.Name})."); +// await providers!.CreateIfNotExists(provider.Provider); +// await thumbnails!.Validate(provider.Provider); +// } +// } +// +// public Task> GetPossibleParameters() +// { +// return Task.FromResult>(null); +// } +// +// public int? Progress() +// { +// return null; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo/Tasks/PluginLoader.cs b/Kyoo/Tasks/PluginLoader.cs index b3f1c064..839e2f1e 100644 --- a/Kyoo/Tasks/PluginLoader.cs +++ b/Kyoo/Tasks/PluginLoader.cs @@ -1,37 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Tasks -{ - public class PluginLoader : ITask - { - public string Slug => "reload-plugin"; - public string Name => "Reload plugins"; - public string Description => "Reload all plugins from the plugin folder."; - public string HelpMessage => null; - public bool RunOnStartup => true; - public int Priority => Int32.MaxValue; - public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) - { - using IServiceScope serviceScope = serviceProvider.CreateScope(); - IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); - pluginManager.ReloadPlugins(); - return Task.CompletedTask; - } - - public Task> GetPossibleParameters() - { - return Task.FromResult>(null); - } - - public int? Progress() - { - return null; - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.Threading; +// using System.Threading.Tasks; +// using Kyoo.Controllers; +// using Kyoo.Models; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kyoo.Tasks +// { +// public class PluginLoader : ITask +// { +// public string Slug => "reload-plugin"; +// public string Name => "Reload plugins"; +// public string Description => "Reload all plugins from the plugin folder."; +// public string HelpMessage => null; +// public bool RunOnStartup => true; +// public int Priority => Int32.MaxValue; +// public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) +// { +// using IServiceScope serviceScope = serviceProvider.CreateScope(); +// IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); +// pluginManager.ReloadPlugins(); +// return Task.CompletedTask; +// } +// +// public Task> GetPossibleParameters() +// { +// return Task.FromResult>(null); +// } +// +// public int? Progress() +// { +// return null; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo/Views/LibraryApi.cs b/Kyoo/Views/LibraryApi.cs index 93081db6..4ec44ea1 100644 --- a/Kyoo/Views/LibraryApi.cs +++ b/Kyoo/Views/LibraryApi.cs @@ -32,7 +32,7 @@ namespace Kyoo.Api { ActionResult result = await base.Create(resource); if (result.Value != null) - _taskManager.StartTask("scan", result.Value.Slug); + _taskManager.StartTask("scan", new Dictionary {{"slug", result.Value.Slug}}); return result; } diff --git a/Kyoo/Views/TaskAPI.cs b/Kyoo/Views/TaskAPI.cs deleted file mode 100644 index 8e531bf9..00000000 --- a/Kyoo/Views/TaskAPI.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - [Route("api/[controller]")] - [ApiController] - public class TaskController : ControllerBase - { - private readonly ITaskManager _taskManager; - - public TaskController(ITaskManager taskManager) - { - _taskManager = taskManager; - } - - - [HttpGet("{taskSlug}/{*args}")] - [HttpPut("{taskSlug}/{*args}")] - [Authorize(Policy="Admin")] - public IActionResult RunTask(string taskSlug, string args = null) - { - if (_taskManager.StartTask(taskSlug, args)) - return Ok(); - return NotFound(); - } - } -} diff --git a/Kyoo/Views/TaskApi.cs b/Kyoo/Views/TaskApi.cs new file mode 100644 index 00000000..56b37b3d --- /dev/null +++ b/Kyoo/Views/TaskApi.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Kyoo.Controllers; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; + +namespace Kyoo.Api +{ + [Route("api/task")] + [Route("api/tasks")] + [ApiController] + [Authorize(Policy="Admin")] + public class TaskApi : ControllerBase + { + private readonly ITaskManager _taskManager; + + public TaskApi(ITaskManager taskManager) + { + _taskManager = taskManager; + } + + + [HttpGet] + public ActionResult> GetTasks() + { + return Ok(_taskManager.GetAllTasks()); + } + + [HttpGet("{taskSlug}")] + [HttpPut("{taskSlug}")] + public IActionResult RunTask(string taskSlug, [FromQuery] Dictionary args) + { + try + { + _taskManager.StartTask(taskSlug, args); + return Ok(); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} diff --git a/Kyoo/settings.json b/Kyoo/settings.json index bdd2f362..ef2aeb46 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -17,10 +17,12 @@ "logLevel": { "default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Kyoo": "Trace" } }, + "parallelTasks": "1", "scheduledTasks": { From 833447ded83c006c677814f2d0dc8980fe8fcd63 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 28 Apr 2021 23:40:22 +0200 Subject: [PATCH 03/25] Reworking the plugin interface and moving the database to an external dll --- Kyoo.Common/Controllers/IPlugin.cs | 44 +- Kyoo.Common/Controllers/ITaskManager.cs | 2 +- Kyoo.Common/Kyoo.Common.csproj | 1 + Kyoo.Common/Module.cs | 9 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 18 + Kyoo.Postgresql/PostgresContext.cs | 76 ++ Kyoo.Postgresql/PostgresModule.cs | 49 ++ Kyoo.Tests/Library/TestContext.cs | 158 ++-- Kyoo.sln | 6 + .../Repositories/LibraryItemRepository.cs | 22 +- .../Repositories/PeopleRepository.cs | 6 +- .../Repositories/SeasonRepository.cs | 7 +- .../Repositories/ShowRepository.cs | 11 +- Kyoo/Controllers/TaskManager.cs | 8 +- Kyoo/CoreModule.cs | 62 +- Kyoo/Models/DatabaseContext.cs | 20 +- .../20210420221509_Initial.Designer.cs | 785 ------------------ .../Internal/20210420221509_Initial.cs | 607 -------------- .../Internal/DatabaseContextModelSnapshot.cs | 783 ----------------- Kyoo/Startup.cs | 48 +- Kyoo/Tasks/Crawler.cs | 3 +- 21 files changed, 365 insertions(+), 2360 deletions(-) create mode 100644 Kyoo.Postgresql/Kyoo.Postgresql.csproj create mode 100644 Kyoo.Postgresql/PostgresContext.cs create mode 100644 Kyoo.Postgresql/PostgresModule.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index a6949083..e4a058af 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -1,8 +1,14 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Unity; + namespace Kyoo.Controllers { /// /// A common interface used to discord plugins /// + [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] public interface IPlugin { /// @@ -19,21 +25,35 @@ namespace Kyoo.Controllers /// The description of this plugin. This will be displayed on the "installed plugins" page. /// string Description { get; } - - + /// - /// A configure method that will be runned on plugin's startup. + /// A list of services that are provided by this service. This allow other plugins to declare dependencies. /// /// - /// You can have use any services as parameter, they will be injected from the service provider - /// You can add managed types or any type you like using the IUnityContainer like so: - /// - /// public static void Configure(IUnityContainer services) - /// { - /// services.AddTask<MyTask>() - /// } - /// + /// The format should be the name of the interface ':' and the name of the implementation. + /// For a plugins that provide a new service named IService with a default implementation named Koala, that would + /// be "IService:Koala". /// - static void Configure() { } + string[] Provides { get; } + + /// + /// A list of services that are required by this service. + /// The Core will warn the user that this plugin can't be loaded if a required service is not found. + /// + /// + /// This is the same format as but you may leave a blank implementation's name if you don't need a special one. + /// For example, if you need a service named IService but you don't care what implementation it will be, you can use + /// "IService:" + /// + string[] Requires { get; } + + /// + /// A configure method that will be run on plugin's startup. + /// + /// A unity container to register new services. + /// The configuration, if you need values at config time (database connection strings...) + /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities. + /// True if the app should run in debug mode. + void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs index 7f3b6013..1197f049 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -18,7 +18,7 @@ namespace Kyoo.Controllers /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit. /// If the number of arguments is invalid or if an argument can't be converted. /// The task could not be found. - void StartTask(string taskSlug, Dictionary arguments); + void StartTask(string taskSlug, Dictionary arguments = null); /// /// Get all currently running tasks diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 3f4fdbba..b9417da8 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -23,6 +23,7 @@ + diff --git a/Kyoo.Common/Module.cs b/Kyoo.Common/Module.cs index 41b5859c..639e7c2a 100644 --- a/Kyoo.Common/Module.cs +++ b/Kyoo.Common/Module.cs @@ -1,3 +1,4 @@ +using System; using Kyoo.Controllers; using Unity; @@ -11,14 +12,14 @@ namespace Kyoo /// /// Register a new task to the container. /// - /// The container + /// The container /// The type of the task /// The initial container. - public static IUnityContainer AddTask(this IUnityContainer services) + public static IUnityContainer RegisterTask(this IUnityContainer container) where T : class, ITask { - services.RegisterSingleton(); - return services; + container.RegisterType(); + return container; } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj new file mode 100644 index 00000000..e0089c22 --- /dev/null +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs new file mode 100644 index 00000000..9d20cf53 --- /dev/null +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -0,0 +1,76 @@ +using System; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; + +namespace Kyoo.Postgresql +{ + /// + /// A postgresql implementation of . + /// + public class PostgresContext : DatabaseContext + { + /// + /// The connection string to use. + /// + private readonly string _connection; + + /// + /// Is this instance in debug mode? + /// + private readonly bool _debugMode; + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + public PostgresContext() + { + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + } + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + /// The connection string to use + /// Is this instance in debug mode? + public PostgresContext(string connection, bool debugMode) + { + _connection = connection; + _debugMode = debugMode; + } + + /// + /// Set connection information for this database context + /// + /// An option builder to fill. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(_connection); + if (_debugMode) + optionsBuilder.EnableDetailedErrors() + .EnableSensitiveDataLogging(); + } + + /// + /// Set database parameters to support every types of Kyoo. + /// + /// The database's model builder. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + + base.OnModelCreating(modelBuilder); + } + + /// + protected override bool IsDuplicateException(Exception ex) + { + return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; + } + } +} \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs new file mode 100644 index 00000000..23549460 --- /dev/null +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -0,0 +1,49 @@ +using System; +using Kyoo.Controllers; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Unity; +using Unity.Injection; +using Unity.Lifetime; +using Unity.Resolution; + +namespace Kyoo.Postgresql +{ + /// + /// A module to add postgresql capacity to the app. + /// + public class PostgresModule : IPlugin + { + /// + public string Slug => "postgresql"; + + /// + public string Name => "Postgresql"; + + /// + public string Description => "A database context for postgresql."; + + /// + public string[] Provides => new[] + { + $"{nameof(DatabaseContext)}:{nameof(PostgresContext)}" + }; + + /// + public string[] Requires => Array.Empty(); + + /// + public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) + { + // options.UseNpgsql(_configuration.GetDatabaseConnection()); + // // // .EnableSensitiveDataLogging() + // // // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); + + container.RegisterFactory(_ => + { + return new PostgresContext(config.GetDatabaseConnection(), debugMode); + }); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index c9a83ad0..e3cabc03 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -1,79 +1,79 @@ -using Kyoo.Models; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Kyoo.Tests -{ - /// - /// Class responsible to fill and create in memory databases for unit tests. - /// - public class TestContext - { - /// - /// The context's options that specify to use an in memory Sqlite database. - /// - private readonly DbContextOptions _context; - - /// - /// Create a new database and fill it with informations. - /// - public TestContext() - { - SqliteConnection connection = new("DataSource=:memory:"); - connection.Open(); - - try - { - _context = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - FillDatabase(); - } - finally - { - connection.Close(); - } - } - - /// - /// Fill the database with pre defined values using a clean context. - /// - private void FillDatabase() - { - using DatabaseContext context = new(_context); - context.Shows.Add(new Show - { - ID = 67, - Slug = "anohana", - Title = "Anohana: The Flower We Saw That Day", - Aliases = new[] - { - "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", - "AnoHana", - "We Still Don't Know the Name of the Flower We Saw That Day." - }, - Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + - "In time, however, these childhood friends drifted apart, and when they became high " + - "school students, they had long ceased to think of each other as friends.", - Status = Status.Finished, - TrailerUrl = null, - StartYear = 2011, - EndYear = 2011, - Poster = "poster", - Logo = "logo", - Backdrop = "backdrop", - IsMovie = false, - Studio = null - }); - } - - /// - /// Get a new databse context connected to a in memory Sqlite databse. - /// - /// A valid DatabaseContext - public DatabaseContext New() - { - return new(_context); - } - } -} \ No newline at end of file +// using Kyoo.Models; +// using Microsoft.Data.Sqlite; +// using Microsoft.EntityFrameworkCore; +// +// namespace Kyoo.Tests +// { +// /// +// /// Class responsible to fill and create in memory databases for unit tests. +// /// +// public class TestContext +// { +// /// +// /// The context's options that specify to use an in memory Sqlite database. +// /// +// private readonly DbContextOptions _context; +// +// /// +// /// Create a new database and fill it with information. +// /// +// public TestContext() +// { +// SqliteConnection connection = new("DataSource=:memory:"); +// connection.Open(); +// +// try +// { +// _context = new DbContextOptionsBuilder() +// .UseSqlite(connection) +// .Options; +// FillDatabase(); +// } +// finally +// { +// connection.Close(); +// } +// } +// +// /// +// /// Fill the database with pre defined values using a clean context. +// /// +// private void FillDatabase() +// { +// using DatabaseContext context = new(_context); +// context.Shows.Add(new Show +// { +// ID = 67, +// Slug = "anohana", +// Title = "Anohana: The Flower We Saw That Day", +// Aliases = new[] +// { +// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", +// "AnoHana", +// "We Still Don't Know the Name of the Flower We Saw That Day." +// }, +// Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + +// "In time, however, these childhood friends drifted apart, and when they became high " + +// "school students, they had long ceased to think of each other as friends.", +// Status = Status.Finished, +// TrailerUrl = null, +// StartYear = 2011, +// EndYear = 2011, +// Poster = "poster", +// Logo = "logo", +// Backdrop = "backdrop", +// IsMovie = false, +// Studio = null +// }); +// } +// +// /// +// /// Get a new database context connected to a in memory Sqlite database. +// /// +// /// A valid DatabaseContext +// public DatabaseContext New() +// { +// return new(_context); +// } +// } +// } \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index 0eb53fe3..79ee6e7d 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.Comm EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Kyoo.Tests.csproj", "{D179D5FF-9F75-4B27-8E27-0DBDF1806611}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,5 +31,9 @@ Global {D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Debug|Any CPU.Build.0 = Debug|Any CPU {D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.ActiveCfg = Release|Any CPU {D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.Build.0 = Release|Any CPU + {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 43afd407..c1db8e46 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { @@ -20,10 +19,6 @@ namespace Kyoo.Controllers /// private readonly DatabaseContext _database; /// - /// A provider repository to handle externalID creation and deletion - /// - private readonly IProviderRepository _providers; - /// /// A lazy loaded library repository to validate queries (check if a library does exist) /// private readonly Lazy _libraries; @@ -44,18 +39,19 @@ namespace Kyoo.Controllers /// Create a new . /// /// The databse instance - /// A provider repository - /// A service provider to lazilly request a library, show or collection repository. + /// A lazy loaded library repository + /// A lazy loaded show repository + /// A lazy loaded collection repository public LibraryItemRepository(DatabaseContext database, - IProviderRepository providers, - IServiceProvider services) + Lazy libraries, + Lazy shows, + Lazy collections) : base(database) { _database = database; - _providers = providers; - _libraries = new Lazy(services.GetRequiredService); - _shows = new Lazy(services.GetRequiredService); - _collections = new Lazy(services.GetRequiredService); + _libraries = libraries; + _shows = shows; + _collections = collections; } diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 526a3286..6de72a4b 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -36,15 +36,15 @@ namespace Kyoo.Controllers /// /// The database handle /// A provider repository - /// A service provider to lazy load a show repository + /// A lazy loaded show repository public PeopleRepository(DatabaseContext database, IProviderRepository providers, - IServiceProvider services) + Lazy shows) : base(database) { _database = database; _providers = providers; - _shows = new Lazy(services.GetRequiredService); + _shows = shows; } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index e35042ad..b0a61c90 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { @@ -44,17 +43,17 @@ namespace Kyoo.Controllers /// The database handle that will be used /// A provider repository /// A show repository - /// A service provider to lazilly request an episode repository. + /// A lazy loaded episode repository. public SeasonRepository(DatabaseContext database, IProviderRepository providers, IShowRepository shows, - IServiceProvider services) + Lazy episodes) : base(database) { _database = database; _providers = providers; _shows = shows; - _episodes = new Lazy(services.GetRequiredService); + _episodes = episodes; } diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 1129cd07..07d2cafd 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { @@ -54,13 +53,15 @@ namespace Kyoo.Controllers /// A people repository /// A genres repository /// A provider repository - /// A service provider to lazilly request a season and an episode repository + /// A lazy loaded season repository + /// A lazy loaded episode repository public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, IGenreRepository genres, IProviderRepository providers, - IServiceProvider services) + Lazy seasons, + Lazy episodes) : base(database) { _database = database; @@ -68,8 +69,8 @@ namespace Kyoo.Controllers _people = people; _genres = genres; _providers = providers; - _seasons = new Lazy(services.GetRequiredService); - _episodes = new Lazy(services.GetRequiredService); + _seasons = seasons; + _episodes = episodes; } diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index e93c050f..a625dcf2 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -62,10 +62,10 @@ namespace Kyoo.Controllers IConfiguration configuration, ILogger logger) { - _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); _container = container; _configuration = configuration.GetSection("scheduledTasks"); _logger = logger; + _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); } @@ -179,8 +179,10 @@ namespace Kyoo.Controllers } /// - public void StartTask(string taskSlug, Dictionary arguments) + public void StartTask(string taskSlug, Dictionary arguments = null) { + arguments ??= new Dictionary(); + int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); if (index == -1) throw new ItemNotFound($"No task found with the slug {taskSlug}"); @@ -216,7 +218,7 @@ namespace Kyoo.Controllers /// public void ReloadTasks() { - _tasks = _container.ResolveAll().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + _tasks = _container.Resolve>().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); EnqueueStartupTasks(); } } diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index d194a267..26e12c80 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -1,10 +1,14 @@ using Kyoo.Controllers; +using Kyoo.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Unity; +using Unity.Lifetime; namespace Kyoo { /// - /// The core module ccontaining default implementations + /// The core module containing default implementations /// public class CoreModule : IPlugin { @@ -17,10 +21,60 @@ namespace Kyoo /// public string Description => "The core module containing default implementations."; - /// - public static void Configure(IUnityContainer container) + /// + public string[] Provides => new[] { - container.AddTask(); + $"{nameof(IFileManager)}:file", + $"{nameof(ITranscoder)}:{nameof(Transcoder)}", + $"{nameof(IThumbnailsManager)}:{nameof(ThumbnailsManager)}", + $"{nameof(IProviderManager)}:{nameof(ProviderManager)}", + $"{nameof(IPluginManager)}:{nameof(PluginManager)}", + $"{nameof(ITaskManager)}:{nameof(TaskManager)}", + $"{nameof(ILibraryManager)}:{nameof(LibraryManager)}", + $"{nameof(ILibraryRepository)}:{nameof(LibraryRepository)}", + $"{nameof(ILibraryItemRepository)}:{nameof(LibraryItemRepository)}", + $"{nameof(ICollectionRepository)}:{nameof(CollectionRepository)}", + $"{nameof(IShowRepository)}:{nameof(ShowRepository)}", + $"{nameof(ISeasonRepository)}:{nameof(SeasonRepository)}", + $"{nameof(IEpisodeRepository)}:{nameof(EpisodeRepository)}", + $"{nameof(ITrackRepository)}:{nameof(TrackRepository)}", + $"{nameof(IPeopleRepository)}:{nameof(PeopleRepository)}", + $"{nameof(IStudioRepository)}:{nameof(StudioRepository)}", + $"{nameof(IGenreRepository)}:{nameof(GenreRepository)}", + $"{nameof(IProviderRepository)}:{nameof(ProviderRepository)}" + }; + + /// + public string[] Requires => new[] + { + "DatabaseContext:" + }; + + /// + public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) + { + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterType(new SingletonLifetimeManager()); + + container.RegisterType(new HierarchicalLifetimeManager()); + + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterType(new HierarchicalLifetimeManager()); + + container.RegisterTask(); } } } \ No newline at end of file diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index 577e9903..caf8933e 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -7,17 +7,17 @@ using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Npgsql; namespace Kyoo { /// /// The database handle used for all local repositories. + /// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic. /// /// /// It should not be used directly, to access the database use a or repositories. /// - public class DatabaseContext : DbContext + public abstract class DatabaseContext : DbContext { /// /// All libraries of Kyoo. See . @@ -89,10 +89,6 @@ namespace Kyoo /// public DatabaseContext() { - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ChangeTracker.LazyLoadingEnabled = false; } @@ -100,7 +96,7 @@ namespace Kyoo /// /// Create a new . /// - /// Connection options to use (witch databse provider to use, connection strings...) + /// Connection options to use (witch database provider to use, connection strings...) public DatabaseContext(DbContextOptions options) : base(options) { @@ -116,10 +112,6 @@ namespace Kyoo { base.OnModelCreating(modelBuilder); - modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); - modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); @@ -467,13 +459,9 @@ namespace Kyoo /// /// Check if the exception is a duplicated exception. /// - /// WARNING: this only works for PostgreSQL /// The exception to check /// True if the exception is a duplicate exception. False otherwise - private static bool IsDuplicateException(Exception ex) - { - return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; - } + protected abstract bool IsDuplicateException(Exception ex); /// /// Delete every changes that are on this context. diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs deleted file mode 100644 index c27925c9..00000000 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs +++ /dev/null @@ -1,785 +0,0 @@ -// -using System; -using Kyoo; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.Internal -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20210420221509_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) - .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) - .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("Kyoo.Models.Collection", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Collections"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AbsoluteNumber") - .HasColumnType("integer"); - - b.Property("EpisodeNumber") - .HasColumnType("integer"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); - - b.Property("Runtime") - .HasColumnType("integer"); - - b.Property("SeasonID") - .HasColumnType("integer"); - - b.Property("SeasonNumber") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Thumb") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); - - b.ToTable("Episodes"); - }); - - modelBuilder.Entity("Kyoo.Models.Genre", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Genres"); - }); - - modelBuilder.Entity("Kyoo.Models.Library", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Paths") - .HasColumnType("text[]"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Libraries"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.MetadataID", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("DataID") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); - - b.Property("Link") - .HasColumnType("text"); - - b.Property("PeopleID") - .HasColumnType("integer"); - - b.Property("ProviderID") - .HasColumnType("integer"); - - b.Property("SeasonID") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("EpisodeID"); - - b.HasIndex("PeopleID"); - - b.HasIndex("ProviderID"); - - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); - }); - - modelBuilder.Entity("Kyoo.Models.People", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("People"); - }); - - modelBuilder.Entity("Kyoo.Models.PeopleRole", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("PeopleID") - .HasColumnType("integer"); - - b.Property("Role") - .HasColumnType("text"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("PeopleID"); - - b.HasIndex("ShowID"); - - b.ToTable("PeopleRoles"); - }); - - modelBuilder.Entity("Kyoo.Models.Provider", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Logo") - .HasColumnType("text"); - - b.Property("LogoExtension") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Providers"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("SeasonNumber") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("Year") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); - - b.ToTable("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Aliases") - .HasColumnType("text[]"); - - b.Property("Backdrop") - .HasColumnType("text"); - - b.Property("EndYear") - .HasColumnType("integer"); - - b.Property("IsMovie") - .HasColumnType("boolean"); - - b.Property("Logo") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartYear") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StudioID") - .HasColumnType("integer"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("TrailerUrl") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.HasIndex("StudioID"); - - b.ToTable("Shows"); - }); - - modelBuilder.Entity("Kyoo.Models.Studio", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Studios"); - }); - - modelBuilder.Entity("Kyoo.Models.Track", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Codec") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsExternal") - .HasColumnType("boolean"); - - b.Property("IsForced") - .HasColumnType("boolean"); - - b.Property("Language") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("TrackIndex") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.HasOne("Kyoo.Models.Season", "Season") - .WithMany("Episodes") - .HasForeignKey("SeasonID"); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("Episodes") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Collection", "First") - .WithMany("ShowLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Second") - .WithMany("CollectionLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("CollectionLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Collection", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("ProviderLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Provider", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("ShowLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Show", "First") - .WithMany("GenreLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Genre", "Second") - .WithMany("ShowLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.MetadataID", b => - { - b.HasOne("Kyoo.Models.Episode", "Episode") - .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Season", "Season") - .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); - - b.Navigation("Episode"); - - b.Navigation("People"); - - b.Navigation("Provider"); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.PeopleRole", b => - { - b.HasOne("Kyoo.Models.People", "People") - .WithMany("Roles") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("People") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("People"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.HasOne("Kyoo.Models.Studio", "Studio") - .WithMany("Shows") - .HasForeignKey("StudioID"); - - b.Navigation("Studio"); - }); - - modelBuilder.Entity("Kyoo.Models.Track", b => - { - b.HasOne("Kyoo.Models.Episode", "Episode") - .WithMany("Tracks") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("Kyoo.Models.Collection", b => - { - b.Navigation("LibraryLinks"); - - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Tracks"); - }); - - modelBuilder.Entity("Kyoo.Models.Genre", b => - { - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Library", b => - { - b.Navigation("CollectionLinks"); - - b.Navigation("ProviderLinks"); - - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.People", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Roles"); - }); - - modelBuilder.Entity("Kyoo.Models.Provider", b => - { - b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.Navigation("CollectionLinks"); - - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - - b.Navigation("GenreLinks"); - - b.Navigation("LibraryLinks"); - - b.Navigation("People"); - - b.Navigation("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Models.Studio", b => - { - b.Navigation("Shows"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs deleted file mode 100644 index 56051cb1..00000000 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs +++ /dev/null @@ -1,607 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.Internal -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:item_type", "show,movie,collection") - .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") - .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); - - migrationBuilder.CreateTable( - name: "Collections", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Poster = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Collections", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Genres", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Genres", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Libraries", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Paths = table.Column(type: "text[]", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Libraries", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "People", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Poster = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_People", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Providers", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Logo = table.Column(type: "text", nullable: true), - LogoExtension = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Providers", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Studios", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Studios", x => x.ID); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Collections_SecondID", - column: x => x.SecondID, - principalTable: "Collections", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Providers_SecondID", - column: x => x.SecondID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Shows", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Title = table.Column(type: "text", nullable: true), - Aliases = table.Column(type: "text[]", nullable: true), - Path = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - Status = table.Column(type: "integer", nullable: true), - TrailerUrl = table.Column(type: "text", nullable: true), - StartYear = table.Column(type: "integer", nullable: true), - EndYear = table.Column(type: "integer", nullable: true), - Poster = table.Column(type: "text", nullable: true), - Logo = table.Column(type: "text", nullable: true), - Backdrop = table.Column(type: "text", nullable: true), - IsMovie = table.Column(type: "boolean", nullable: false), - StudioID = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Shows", x => x.ID); - table.ForeignKey( - name: "FK_Shows_Studios_StudioID", - column: x => x.StudioID, - principalTable: "Studios", - principalColumn: "ID", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Collections_FirstID", - column: x => x.FirstID, - principalTable: "Collections", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_SecondID", - column: x => x.SecondID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Libraries_FirstID", - column: x => x.FirstID, - principalTable: "Libraries", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_SecondID", - column: x => x.SecondID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Link", - columns: table => new - { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); - table.ForeignKey( - name: "FK_Link_Genres_SecondID", - column: x => x.SecondID, - principalTable: "Genres", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Link_Shows_FirstID", - column: x => x.FirstID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PeopleRoles", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - PeopleID = table.Column(type: "integer", nullable: false), - ShowID = table.Column(type: "integer", nullable: false), - Role = table.Column(type: "text", nullable: true), - Type = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_PeopleRoles", x => x.ID); - table.ForeignKey( - name: "FK_PeopleRoles_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PeopleRoles_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Seasons", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ShowID = table.Column(type: "integer", nullable: false), - SeasonNumber = table.Column(type: "integer", nullable: false), - Title = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - Year = table.Column(type: "integer", nullable: true), - Poster = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Seasons", x => x.ID); - table.ForeignKey( - name: "FK_Seasons_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Episodes", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ShowID = table.Column(type: "integer", nullable: false), - SeasonID = table.Column(type: "integer", nullable: true), - SeasonNumber = table.Column(type: "integer", nullable: false), - EpisodeNumber = table.Column(type: "integer", nullable: false), - AbsoluteNumber = table.Column(type: "integer", nullable: false), - Path = table.Column(type: "text", nullable: true), - Thumb = table.Column(type: "text", nullable: true), - Title = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true), - Runtime = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Episodes", x => x.ID); - table.ForeignKey( - name: "FK_Episodes_Seasons_SeasonID", - column: x => x.SeasonID, - principalTable: "Seasons", - principalColumn: "ID", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Episodes_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MetadataIds", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderID = table.Column(type: "integer", nullable: false), - ShowID = table.Column(type: "integer", nullable: true), - EpisodeID = table.Column(type: "integer", nullable: true), - SeasonID = table.Column(type: "integer", nullable: true), - PeopleID = table.Column(type: "integer", nullable: true), - DataID = table.Column(type: "text", nullable: true), - Link = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataIds", x => x.ID); - table.ForeignKey( - name: "FK_MetadataIds_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Providers_ProviderID", - column: x => x.ProviderID, - principalTable: "Providers", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Seasons_SeasonID", - column: x => x.SeasonID, - principalTable: "Seasons", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Tracks", - columns: table => new - { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - EpisodeID = table.Column(type: "integer", nullable: false), - TrackIndex = table.Column(type: "integer", nullable: false), - IsDefault = table.Column(type: "boolean", nullable: false), - IsForced = table.Column(type: "boolean", nullable: false), - IsExternal = table.Column(type: "boolean", nullable: false), - Title = table.Column(type: "text", nullable: true), - Language = table.Column(type: "text", nullable: true), - Codec = table.Column(type: "text", nullable: true), - Path = table.Column(type: "text", nullable: true), - Type = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tracks", x => x.ID); - table.ForeignKey( - name: "FK_Tracks_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Collections_Slug", - table: "Collections", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Episodes_SeasonID", - table: "Episodes", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_Episodes_ShowID_SeasonNumber_EpisodeNumber_AbsoluteNumber", - table: "Episodes", - columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Genres_Slug", - table: "Genres", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Libraries_Slug", - table: "Libraries", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_Link_SecondID", - table: "Link", - column: "SecondID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_EpisodeID", - table: "MetadataIds", - column: "EpisodeID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_PeopleID", - table: "MetadataIds", - column: "PeopleID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ProviderID", - table: "MetadataIds", - column: "ProviderID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_SeasonID", - table: "MetadataIds", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ShowID", - table: "MetadataIds", - column: "ShowID"); - - migrationBuilder.CreateIndex( - name: "IX_People_Slug", - table: "People", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_PeopleRoles_PeopleID", - table: "PeopleRoles", - column: "PeopleID"); - - migrationBuilder.CreateIndex( - name: "IX_PeopleRoles_ShowID", - table: "PeopleRoles", - column: "ShowID"); - - migrationBuilder.CreateIndex( - name: "IX_Providers_Slug", - table: "Providers", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Seasons_ShowID_SeasonNumber", - table: "Seasons", - columns: new[] { "ShowID", "SeasonNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Shows_Slug", - table: "Shows", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Shows_StudioID", - table: "Shows", - column: "StudioID"); - - migrationBuilder.CreateIndex( - name: "IX_Studios_Slug", - table: "Studios", - column: "Slug", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", - table: "Tracks", - columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "Link"); - - migrationBuilder.DropTable( - name: "MetadataIds"); - - migrationBuilder.DropTable( - name: "PeopleRoles"); - - migrationBuilder.DropTable( - name: "Tracks"); - - migrationBuilder.DropTable( - name: "Collections"); - - migrationBuilder.DropTable( - name: "Libraries"); - - migrationBuilder.DropTable( - name: "Genres"); - - migrationBuilder.DropTable( - name: "Providers"); - - migrationBuilder.DropTable( - name: "People"); - - migrationBuilder.DropTable( - name: "Episodes"); - - migrationBuilder.DropTable( - name: "Seasons"); - - migrationBuilder.DropTable( - name: "Shows"); - - migrationBuilder.DropTable( - name: "Studios"); - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs deleted file mode 100644 index 11d6b186..00000000 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ /dev/null @@ -1,783 +0,0 @@ -// -using System; -using Kyoo; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.Internal -{ - [DbContext(typeof(DatabaseContext))] - partial class DatabaseContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) - .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) - .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("Kyoo.Models.Collection", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Collections"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AbsoluteNumber") - .HasColumnType("integer"); - - b.Property("EpisodeNumber") - .HasColumnType("integer"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); - - b.Property("Runtime") - .HasColumnType("integer"); - - b.Property("SeasonID") - .HasColumnType("integer"); - - b.Property("SeasonNumber") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Thumb") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); - - b.ToTable("Episodes"); - }); - - modelBuilder.Entity("Kyoo.Models.Genre", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Genres"); - }); - - modelBuilder.Entity("Kyoo.Models.Library", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Paths") - .HasColumnType("text[]"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Libraries"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.Property("FirstID") - .HasColumnType("integer"); - - b.Property("SecondID") - .HasColumnType("integer"); - - b.HasKey("FirstID", "SecondID"); - - b.HasIndex("SecondID"); - - b.ToTable("Link"); - }); - - modelBuilder.Entity("Kyoo.Models.MetadataID", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("DataID") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); - - b.Property("Link") - .HasColumnType("text"); - - b.Property("PeopleID") - .HasColumnType("integer"); - - b.Property("ProviderID") - .HasColumnType("integer"); - - b.Property("SeasonID") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("EpisodeID"); - - b.HasIndex("PeopleID"); - - b.HasIndex("ProviderID"); - - b.HasIndex("SeasonID"); - - b.HasIndex("ShowID"); - - b.ToTable("MetadataIds"); - }); - - modelBuilder.Entity("Kyoo.Models.People", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("People"); - }); - - modelBuilder.Entity("Kyoo.Models.PeopleRole", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("PeopleID") - .HasColumnType("integer"); - - b.Property("Role") - .HasColumnType("text"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("PeopleID"); - - b.HasIndex("ShowID"); - - b.ToTable("PeopleRoles"); - }); - - modelBuilder.Entity("Kyoo.Models.Provider", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Logo") - .HasColumnType("text"); - - b.Property("LogoExtension") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Providers"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("SeasonNumber") - .HasColumnType("integer"); - - b.Property("ShowID") - .HasColumnType("integer"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("Year") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); - - b.ToTable("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Aliases") - .HasColumnType("text[]"); - - b.Property("Backdrop") - .HasColumnType("text"); - - b.Property("EndYear") - .HasColumnType("integer"); - - b.Property("IsMovie") - .HasColumnType("boolean"); - - b.Property("Logo") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("Poster") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartYear") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StudioID") - .HasColumnType("integer"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("TrailerUrl") - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.HasIndex("StudioID"); - - b.ToTable("Shows"); - }); - - modelBuilder.Entity("Kyoo.Models.Studio", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ID"); - - b.HasIndex("Slug") - .IsUnique(); - - b.ToTable("Studios"); - }); - - modelBuilder.Entity("Kyoo.Models.Track", b => - { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Codec") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsExternal") - .HasColumnType("boolean"); - - b.Property("IsForced") - .HasColumnType("boolean"); - - b.Property("Language") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("TrackIndex") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("ID"); - - b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.HasOne("Kyoo.Models.Season", "Season") - .WithMany("Episodes") - .HasForeignKey("SeasonID"); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("Episodes") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Collection", "First") - .WithMany("ShowLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Second") - .WithMany("CollectionLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("CollectionLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Collection", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("ProviderLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Provider", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Library", "First") - .WithMany("ShowLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Second") - .WithMany("LibraryLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.Link", b => - { - b.HasOne("Kyoo.Models.Show", "First") - .WithMany("GenreLinks") - .HasForeignKey("FirstID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Genre", "Second") - .WithMany("ShowLinks") - .HasForeignKey("SecondID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("First"); - - b.Navigation("Second"); - }); - - modelBuilder.Entity("Kyoo.Models.MetadataID", b => - { - b.HasOne("Kyoo.Models.Episode", "Episode") - .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Season", "Season") - .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); - - b.Navigation("Episode"); - - b.Navigation("People"); - - b.Navigation("Provider"); - - b.Navigation("Season"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.PeopleRole", b => - { - b.HasOne("Kyoo.Models.People", "People") - .WithMany("Roles") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("People") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("People"); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.HasOne("Kyoo.Models.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.HasOne("Kyoo.Models.Studio", "Studio") - .WithMany("Shows") - .HasForeignKey("StudioID"); - - b.Navigation("Studio"); - }); - - modelBuilder.Entity("Kyoo.Models.Track", b => - { - b.HasOne("Kyoo.Models.Episode", "Episode") - .WithMany("Tracks") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Episode"); - }); - - modelBuilder.Entity("Kyoo.Models.Collection", b => - { - b.Navigation("LibraryLinks"); - - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Episode", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Tracks"); - }); - - modelBuilder.Entity("Kyoo.Models.Genre", b => - { - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Library", b => - { - b.Navigation("CollectionLinks"); - - b.Navigation("ProviderLinks"); - - b.Navigation("ShowLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.People", b => - { - b.Navigation("ExternalIDs"); - - b.Navigation("Roles"); - }); - - modelBuilder.Entity("Kyoo.Models.Provider", b => - { - b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.Season", b => - { - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - }); - - modelBuilder.Entity("Kyoo.Models.Show", b => - { - b.Navigation("CollectionLinks"); - - b.Navigation("Episodes"); - - b.Navigation("ExternalIDs"); - - b.Navigation("GenreLinks"); - - b.Navigation("LibraryLinks"); - - b.Navigation("People"); - - b.Navigation("Seasons"); - }); - - modelBuilder.Entity("Kyoo.Models.Studio", b => - { - b.Navigation("Shows"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 5f259d24..e5493f92 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Unity; +using Unity.Lifetime; namespace Kyoo { @@ -147,46 +148,11 @@ namespace Kyoo AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } }); - - // TODO Add custom method to the service container and expose those methods to the plugin - // TODO Add for example a AddRepository that will automatically register the complex interface, the IRepository and the IBaseRepository - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHostedService(provider => (TaskManager)provider.GetService()); + services.AddScoped(); } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IUnityContainer container) + + public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -249,8 +215,10 @@ namespace Kyoo spa.UseAngularCliServer("start"); }); - CoreModule.Configure(container); - container.Resolve().ReloadTasks(); + new CoreModule().Configure(container, _configuration, app, env.IsDevelopment()); + container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); + // TODO the reload should re inject components from the constructor. + // TODO fin a way to inject tasks without a IUnityContainer. } } } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index d86f54d9..7fc5699c 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -7,11 +7,12 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Kyoo.Controllers; using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; using Microsoft.Extensions.DependencyInjection; -namespace Kyoo.Controllers +namespace Kyoo.Tasks { public class Crawler : ITask { From 79995ea191180d0cd5022f8dde074dfa3361d54f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 29 Apr 2021 23:59:46 +0200 Subject: [PATCH 04/25] Reworking the plugin manager --- Kyoo.Common/Controllers/ILibraryManager.cs | 26 +-- Kyoo.Common/Controllers/IPlugin.cs | 29 ++-- Kyoo.Common/Controllers/IPluginManager.cs | 32 +++- Kyoo.Common/Controllers/IRepository.cs | 44 ++--- Kyoo.Common/Controllers/ITaskManager.cs | 5 +- Kyoo.Common/Controllers/IThumbnailsManager.cs | 1 - .../Implementations/LibraryManager.cs | 2 +- .../Exceptions/DuplicatedItemException.cs | 23 ++- Kyoo.Common/Models/Exceptions/ItemNotFound.cs | 16 -- .../Exceptions/ItemNotFoundException.cs | 24 +++ .../Exceptions/MissingDependencyException.cs | 20 +++ Kyoo.CommonAPI/CrudApi.cs | 16 +- .../DatabaseContext.cs | 0 {Kyoo => Kyoo.CommonAPI}/Extensions.cs | 7 +- Kyoo.CommonAPI/LocalRepository.cs | 10 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 21 ++- Kyoo.Postgresql/PostgresContext.cs | 1 - Kyoo.Postgresql/PostgresModule.cs | 50 ++++-- Kyoo/Controllers/PluginManager.cs | 154 ++++++++++++------ .../Repositories/EpisodeRepository.cs | 4 +- .../Repositories/LibraryItemRepository.cs | 4 +- .../Repositories/PeopleRepository.cs | 8 +- .../Repositories/SeasonRepository.cs | 4 +- .../Repositories/TrackRepository.cs | 2 +- Kyoo/Controllers/TaskManager.cs | 2 +- Kyoo/CoreModule.cs | 47 +++--- Kyoo/Kyoo.csproj | 6 +- Kyoo/Program.cs | 2 +- Kyoo/Startup.cs | 23 +-- Kyoo/Tasks/Crawler.cs | 4 +- Kyoo/Views/EpisodeApi.cs | 8 +- Kyoo/Views/LibraryItemApi.cs | 2 +- Kyoo/Views/PeopleApi.cs | 4 +- Kyoo/Views/ShowApi.cs | 14 +- Kyoo/Views/TaskApi.cs | 2 +- Kyoo/Views/TrackApi.cs | 4 +- Kyoo/Views/VideoApi.cs | 6 +- Kyoo/Views/WatchApi.cs | 2 +- Kyoo/settings.json | 18 +- 39 files changed, 394 insertions(+), 253 deletions(-) delete mode 100644 Kyoo.Common/Models/Exceptions/ItemNotFound.cs create mode 100644 Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs create mode 100644 Kyoo.Common/Models/Exceptions/MissingDependencyException.cs rename {Kyoo/Models => Kyoo.CommonAPI}/DatabaseContext.cs (100%) rename {Kyoo => Kyoo.CommonAPI}/Extensions.cs (87%) diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index d15987de..6c100622 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -18,7 +18,7 @@ namespace Kyoo.Controllers /// Get the repository corresponding to the T item. /// /// The type you want - /// If the item is not found + /// If the item is not found /// The repository corresponding IRepository GetRepository() where T : class, IResource; @@ -82,7 +82,7 @@ namespace Kyoo.Controllers /// /// The id of the resource /// The type of the resource - /// If the item is not found + /// If the item is not found /// The resource found Task Get(int id) where T : class, IResource; @@ -91,7 +91,7 @@ namespace Kyoo.Controllers /// /// The slug of the resource /// The type of the resource - /// If the item is not found + /// If the item is not found /// The resource found Task Get(string slug) where T : class, IResource; @@ -100,7 +100,7 @@ namespace Kyoo.Controllers /// /// The filter function. /// The type of the resource - /// If the item is not found + /// If the item is not found /// The first resource found that match the where function Task Get(Expression> where) where T : class, IResource; @@ -109,7 +109,7 @@ namespace Kyoo.Controllers /// /// The id of the show /// The season's number - /// If the item is not found + /// If the item is not found /// The season found Task Get(int showID, int seasonNumber); @@ -118,7 +118,7 @@ namespace Kyoo.Controllers /// /// The slug of the show /// The season's number - /// If the item is not found + /// If the item is not found /// The season found Task Get(string showSlug, int seasonNumber); @@ -128,7 +128,7 @@ namespace Kyoo.Controllers /// The id of the show /// The season's number /// The episode's number - /// If the item is not found + /// If the item is not found /// The episode found Task Get(int showID, int seasonNumber, int episodeNumber); @@ -138,7 +138,7 @@ namespace Kyoo.Controllers /// The slug of the show /// The season's number /// The episode's number - /// If the item is not found + /// If the item is not found /// The episode found Task Get(string showSlug, int seasonNumber, int episodeNumber); @@ -147,7 +147,7 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// If the item is not found + /// If the item is not found /// The tracl found Task Get(string slug, StreamType type = StreamType.Unknown); @@ -505,7 +505,7 @@ namespace Kyoo.Controllers /// The resourcce to edit, it's ID can't change. /// Should old properties of the resource be discarded or should null values considered as not changed? /// The type of resources - /// If the item is not found + /// If the item is not found /// The resource edited and completed by database's informations (related items & so on) Task Edit(T item, bool resetOld) where T : class, IResource; @@ -514,7 +514,7 @@ namespace Kyoo.Controllers /// /// The resource to delete /// The type of resource to delete - /// If the item is not found + /// If the item is not found Task Delete(T item) where T : class, IResource; /// @@ -522,7 +522,7 @@ namespace Kyoo.Controllers /// /// The id of the resource to delete /// The type of resource to delete - /// If the item is not found + /// If the item is not found Task Delete(int id) where T : class, IResource; /// @@ -530,7 +530,7 @@ namespace Kyoo.Controllers /// /// The slug of the resource to delete /// The type of resource to delete - /// If the item is not found + /// If the item is not found Task Delete(string slug) where T : class, IResource; } } diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index e4a058af..c1652a4a 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -1,6 +1,6 @@ +using System; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Unity; namespace Kyoo.Controllers @@ -8,6 +8,8 @@ namespace Kyoo.Controllers /// /// A common interface used to discord plugins /// + /// You can inject services in the IPlugin constructor. + /// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment. [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] public interface IPlugin { @@ -30,30 +32,33 @@ namespace Kyoo.Controllers /// A list of services that are provided by this service. This allow other plugins to declare dependencies. /// /// - /// The format should be the name of the interface ':' and the name of the implementation. - /// For a plugins that provide a new service named IService with a default implementation named Koala, that would - /// be "IService:Koala". + /// You should put directly the type that you will register in configure, Kyoo will detect by itself which + /// interfaces are implemented by your type. /// - string[] Provides { get; } + Type[] Provides { get; } /// /// A list of services that are required by this service. /// The Core will warn the user that this plugin can't be loaded if a required service is not found. /// /// - /// This is the same format as but you may leave a blank implementation's name if you don't need a special one. - /// For example, if you need a service named IService but you don't care what implementation it will be, you can use - /// "IService:" + /// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager, + /// put typeof(ILibraryManager). /// - string[] Requires { get; } + Type[] Requires { get; } /// /// A configure method that will be run on plugin's startup. /// /// A unity container to register new services. - /// The configuration, if you need values at config time (database connection strings...) + void Configure(IUnityContainer container); + + /// + /// An optional configuration step to allow a plugin to change asp net configurations. + /// WARNING: This is only called on Kyoo's startup so you must restart the app to apply this changes. + /// /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities. - /// True if the app should run in debug mode. - void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode); + void ConfigureAspNet(IApplicationBuilder app) {} + } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs index 643cce7a..558f86bf 100644 --- a/Kyoo.Common/Controllers/IPluginManager.cs +++ b/Kyoo.Common/Controllers/IPluginManager.cs @@ -1,13 +1,39 @@ using System.Collections.Generic; -using Kyoo.Models; +using Kyoo.Models.Exceptions; namespace Kyoo.Controllers { + /// + /// A manager to load plugins and retrieve information from them. + /// public interface IPluginManager { + /// + /// Get a single plugin that match the type and name given. + /// + /// The name of the plugin + /// The type of the plugin + /// If no plugins match the query + /// A plugin that match the queries public T GetPlugin(string name); - public IEnumerable GetPlugins(); - public IEnumerable GetAllPlugins(); + + /// + /// Get all plugins of the given type. + /// + /// The type of plugins to get + /// A list of plugins matching the given type or an empty list of none match. + public ICollection GetPlugins(); + + /// + /// Get all plugins currently running on Kyoo. This also includes deleted plugins if the app as not been restarted. + /// + /// All plugins currently loaded. + public ICollection GetAllPlugins(); + + /// + /// Load new plugins from the plugin directory. + /// + /// If a plugin can't be loaded because a dependency can't be resolved. public void ReloadPlugins(); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 2ed0b19a..bc5ed0bc 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -127,21 +127,21 @@ namespace Kyoo.Controllers /// Get a resource from it's ID. /// /// The id of the resource - /// If the item could not be found. + /// If the item could not be found. /// The resource found Task Get(int id); /// /// Get a resource from it's slug. /// /// The slug of the resource - /// If the item could not be found. + /// If the item could not be found. /// The resource found Task Get(string slug); /// /// Get the first resource that match the predicate. /// /// A predicate to filter the resource. - /// If the item could not be found. + /// If the item could not be found. /// The resource found Task Get(Expression> where); @@ -221,7 +221,7 @@ namespace Kyoo.Controllers /// /// The resourcce to edit, it's ID can't change. /// Should old properties of the resource be discarded or should null values considered as not changed? - /// If the item is not found + /// If the item is not found /// The resource edited and completed by database's informations (related items & so on) Task Edit([NotNull] T edited, bool resetOld); @@ -229,62 +229,62 @@ namespace Kyoo.Controllers /// Delete a resource by it's ID /// /// The ID of the resource - /// If the item is not found + /// If the item is not found Task Delete(int id); /// /// Delete a resource by it's slug /// /// The slug of the resource - /// If the item is not found + /// If the item is not found Task Delete(string slug); /// /// Delete a resource /// /// The resource to delete - /// If the item is not found + /// If the item is not found Task Delete([NotNull] T obj); /// /// Delete a list of resources. /// /// One or multiple resources to delete - /// If the item is not found + /// If the item is not found Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); /// /// Delete a list of resources. /// /// An enumerable of resources to delete - /// If the item is not found + /// If the item is not found Task DeleteRange(IEnumerable objs); /// /// Delete a list of resources. /// /// One or multiple resources's id - /// If the item is not found + /// If the item is not found Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); /// /// Delete a list of resources. /// /// An enumearble of resources's id - /// If the item is not found + /// If the item is not found Task DeleteRange(IEnumerable ids); /// /// Delete a list of resources. /// /// One or multiple resources's slug - /// If the item is not found + /// If the item is not found Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); /// /// Delete a list of resources. /// /// An enumerable of resources's slug - /// If the item is not found + /// If the item is not found Task DeleteRange(IEnumerable slugs); /// /// Delete a list of resources. /// /// A predicate to filter resources to delete. Every resource that match this will be deleted. - /// If the item is not found + /// If the item is not found Task DeleteRange([NotNull] Expression> where); } @@ -306,7 +306,7 @@ namespace Kyoo.Controllers /// Get a show's slug from it's ID. /// /// The ID of the show - /// If a show with the given ID is not found. + /// If a show with the given ID is not found. /// The show's slug Task GetSlug(int showID); } @@ -321,7 +321,7 @@ namespace Kyoo.Controllers /// /// The id of the show /// The season's number - /// If the item is not found + /// If the item is not found /// The season found Task Get(int showID, int seasonNumber); @@ -330,7 +330,7 @@ namespace Kyoo.Controllers /// /// The slug of the show /// The season's number - /// If the item is not found + /// If the item is not found /// The season found Task Get(string showSlug, int seasonNumber); @@ -362,7 +362,7 @@ namespace Kyoo.Controllers /// The id of the show /// The season's number /// The episode's number - /// If the item is not found + /// If the item is not found /// The episode found Task Get(int showID, int seasonNumber, int episodeNumber); /// @@ -371,7 +371,7 @@ namespace Kyoo.Controllers /// The slug of the show /// The season's number /// The episode's number - /// If the item is not found + /// If the item is not found /// The episode found Task Get(string showSlug, int seasonNumber, int episodeNumber); @@ -397,7 +397,7 @@ namespace Kyoo.Controllers /// /// The id of the show /// The episode's absolute number (The episode number does not reset to 1 after the end of a season. - /// If the item is not found + /// If the item is not found /// The episode found Task GetAbsolute(int showID, int absoluteNumber); /// @@ -405,7 +405,7 @@ namespace Kyoo.Controllers /// /// The slug of the show /// The episode's absolute number (The episode number does not reset to 1 after the end of a season. - /// If the item is not found + /// If the item is not found /// The episode found Task GetAbsolute(string showSlug, int absoluteNumber); } @@ -420,7 +420,7 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// If the item is not found + /// If the item is not found /// The tracl found Task Get(string slug, StreamType type = StreamType.Unknown); diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs index 1197f049..5a0710c6 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Kyoo.Models; using Kyoo.Models.Exceptions; namespace Kyoo.Controllers @@ -17,7 +16,7 @@ namespace Kyoo.Controllers /// The slug of the task to run /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit. /// If the number of arguments is invalid or if an argument can't be converted. - /// The task could not be found. + /// The task could not be found. void StartTask(string taskSlug, Dictionary arguments = null); /// @@ -27,7 +26,7 @@ namespace Kyoo.Controllers ICollection GetRunningTasks(); /// - /// Get all availables tasks + /// Get all available tasks /// /// A list of every tasks that this instance know. ICollection GetAllTasks(); diff --git a/Kyoo.Common/Controllers/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs index 2282981a..ee31498a 100644 --- a/Kyoo.Common/Controllers/IThumbnailsManager.cs +++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs @@ -1,5 +1,4 @@ using Kyoo.Models; -using System.Collections.Generic; using System.Threading.Tasks; using JetBrains.Annotations; diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index 2fb16735..dcf35813 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -66,7 +66,7 @@ namespace Kyoo.Controllers { if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository ret) return ret; - throw new ItemNotFound(); + throw new ItemNotFoundException(); } /// diff --git a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs index 19be8a04..eb85432f 100644 --- a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs +++ b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs @@ -2,18 +2,25 @@ using System; namespace Kyoo.Models.Exceptions { + /// + /// An exception raised when an item already exists in the database. + /// + [Serializable] public class DuplicatedItemException : Exception { - public override string Message { get; } - + /// + /// Create a new with the default message. + /// public DuplicatedItemException() - { - Message = "Already exists in the databse."; - } + : base("Already exists in the database.") + { } + /// + /// Create a new with a custom message. + /// + /// The message to use public DuplicatedItemException(string message) - { - Message = message; - } + : base(message) + { } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/ItemNotFound.cs b/Kyoo.Common/Models/Exceptions/ItemNotFound.cs deleted file mode 100644 index f23cf363..00000000 --- a/Kyoo.Common/Models/Exceptions/ItemNotFound.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Kyoo.Models.Exceptions -{ - public class ItemNotFound : Exception - { - public override string Message { get; } - - public ItemNotFound() {} - - public ItemNotFound(string message) - { - Message = message; - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs b/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs new file mode 100644 index 00000000..a04b60a1 --- /dev/null +++ b/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs @@ -0,0 +1,24 @@ +using System; + +namespace Kyoo.Models.Exceptions +{ + /// + /// An exception raised when an item could not be found. + /// + [Serializable] + public class ItemNotFoundException : Exception + { + /// + /// Create a default with no message. + /// + public ItemNotFoundException() {} + + /// + /// Create a new with a message + /// + /// The message of the exception + public ItemNotFoundException(string message) + : base(message) + { } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs b/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs new file mode 100644 index 00000000..ec459faf --- /dev/null +++ b/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kyoo.Models.Exceptions +{ + /// + /// An exception raised when a plugin requires dependencies that can't be found with the current configuration. + /// + [Serializable] + public class MissingDependencyException : Exception + { + /// + /// Create a new with a custom message + /// + /// The name of the plugin that can't be loaded. + /// The name of the missing dependency. + public MissingDependencyException(string plugin, string dependency) + : base($"No {dependency} are available in kyoo but the plugin {plugin} requires it.") + {} + } +} \ No newline at end of file diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 50dcb588..27ddf66b 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -33,7 +33,7 @@ namespace Kyoo.CommonApi { return await _repository.Get(id); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -47,7 +47,7 @@ namespace Kyoo.CommonApi { return await _repository.Get(slug); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -129,7 +129,7 @@ namespace Kyoo.CommonApi resource.ID = old.ID; return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -144,7 +144,7 @@ namespace Kyoo.CommonApi { return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -160,7 +160,7 @@ namespace Kyoo.CommonApi resource.ID = old.ID; return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -174,7 +174,7 @@ namespace Kyoo.CommonApi { await _repository.Delete(id); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -190,7 +190,7 @@ namespace Kyoo.CommonApi { await _repository.Delete(slug); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -205,7 +205,7 @@ namespace Kyoo.CommonApi { await _repository.DeleteRange(ApiHelper.ParseWhere(where)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs similarity index 100% rename from Kyoo/Models/DatabaseContext.cs rename to Kyoo.CommonAPI/DatabaseContext.cs diff --git a/Kyoo/Extensions.cs b/Kyoo.CommonAPI/Extensions.cs similarity index 87% rename from Kyoo/Extensions.cs rename to Kyoo.CommonAPI/Extensions.cs index 70a100d2..dfb2d4d8 100644 --- a/Kyoo/Extensions.cs +++ b/Kyoo.CommonAPI/Extensions.cs @@ -9,14 +9,15 @@ namespace Kyoo public static class Extensions { /// - /// Get a connection string from the Configuration's section "Databse" + /// Get a connection string from the Configuration's section "Database" /// /// The IConfiguration instance to load. + /// The database's name. /// A parsed connection string - public static string GetDatabaseConnection(this IConfiguration config) + public static string GetDatabaseConnection(this IConfiguration config, string database) { DbConnectionStringBuilder builder = new(); - IConfigurationSection section = config.GetSection("Database"); + IConfigurationSection section = config.GetSection("Database").GetSection(database); foreach (IConfigurationSection child in section.GetChildren()) builder[child.Key] = child.Value; return builder.ConnectionString; diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index c1e14a6e..32b47bd2 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -46,13 +46,13 @@ namespace Kyoo.Controllers /// Get a resource from it's ID and make the instance track it. /// /// The ID of the resource - /// If the item is not found + /// If the item is not found /// The tracked resource with the given ID protected virtual async Task GetWithTracking(int id) { T ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.ID == id); if (ret == null) - throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}"); + throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); return ret; } @@ -61,7 +61,7 @@ namespace Kyoo.Controllers { T ret = await GetOrDefault(id); if (ret == null) - throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}"); + throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); return ret; } @@ -70,7 +70,7 @@ namespace Kyoo.Controllers { T ret = await GetOrDefault(slug); if (ret == null) - throw new ItemNotFound($"No {typeof(T).Name} found with the slug {slug}"); + throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}"); return ret; } @@ -79,7 +79,7 @@ namespace Kyoo.Controllers { T ret = await GetOrDefault(where); if (ret == null) - throw new ItemNotFound($"No {typeof(T).Name} found with the given predicate."); + throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate."); return ret; } diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index e0089c22..5e4d8678 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -2,10 +2,18 @@ net5.0 + $(SolutionDir)/Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + false + false + false + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default - @@ -15,4 +23,15 @@ + + + + all + false + + + all + false + + diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 9d20cf53..7ee49adb 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -1,7 +1,6 @@ using System; using Kyoo.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using Npgsql; namespace Kyoo.Postgresql diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 23549460..c83df51d 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -1,12 +1,9 @@ using System; using Kyoo.Controllers; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Unity; -using Unity.Injection; -using Unity.Lifetime; -using Unity.Resolution; namespace Kyoo.Postgresql { @@ -25,25 +22,42 @@ namespace Kyoo.Postgresql public string Description => "A database context for postgresql."; /// - public string[] Provides => new[] + public Type[] Provides => new[] { - $"{nameof(DatabaseContext)}:{nameof(PostgresContext)}" + typeof(PostgresContext) }; /// - public string[] Requires => Array.Empty(); + public Type[] Requires => Array.Empty(); - /// - public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) + + /// + /// The configuration to use. The database connection string is pulled from it. + /// + private readonly IConfiguration _configuration; + + /// + /// The host environment to check if the app is in debug mode. + /// + private readonly IWebHostEnvironment _environment; + + /// + /// Create a new postgres module instance and use the given configuration and environment. + /// + /// The configuration to use + /// The environment that will be used (if the env is in development mode, more information will be displayed on errors. + public PostgresModule(IConfiguration configuration, IWebHostEnvironment env) { - // options.UseNpgsql(_configuration.GetDatabaseConnection()); - // // // .EnableSensitiveDataLogging() - // // // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); - - container.RegisterFactory(_ => - { - return new PostgresContext(config.GetDatabaseConnection(), debugMode); - }); + _configuration = configuration; + _environment = env; + } + + /// + public void Configure(IUnityContainer container) + { + container.RegisterFactory(_ => new PostgresContext( + _configuration.GetDatabaseConnection("postgres"), + _environment.IsDevelopment())); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 5026e7c3..8b9e9ca2 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -4,100 +4,152 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; -using Kyoo.Models; +using Kyoo.Models.Exceptions; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Unity; namespace Kyoo.Controllers { - public class PluginDependencyLoader : AssemblyLoadContext - { - private readonly AssemblyDependencyResolver _resolver; - - public PluginDependencyLoader(string pluginPath) - { - _resolver = new AssemblyDependencyResolver(pluginPath); - } - - protected override Assembly Load(AssemblyName assemblyName) - { - string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); - if (assemblyPath != null) - return LoadFromAssemblyPath(assemblyPath); - return base.Load(assemblyName); - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - if (libraryPath != null) - return LoadUnmanagedDllFromPath(libraryPath); - return base.LoadUnmanagedDll(unmanagedDllName); - } - } - + /// + /// An implementation of . + /// This is used to load plugins and retrieve information from them. + /// public class PluginManager : IPluginManager { - private readonly IServiceProvider _provider; + /// + /// The unity container. It is given to the Configure method of plugins. + /// + private readonly IUnityContainer _container; + /// + /// The configuration to get the plugin's directory. + /// private readonly IConfiguration _config; - private List _plugins; + /// + /// The logger used by this class. + /// + private readonly ILogger _logger; + + /// + /// The list of plugins that are currently loaded. + /// + private readonly List _plugins = new(); - public PluginManager(IServiceProvider provider, IConfiguration config) + /// + /// Create a new instance. + /// + /// A unity container to allow plugins to register new entries + /// The configuration instance, to get the plugin's directory path. + /// The logger used by this class. + public PluginManager(IUnityContainer container, + IConfiguration config, + ILogger logger) { - _provider = provider; + _container = container; _config = config; + _logger = logger; } + + /// public T GetPlugin(string name) { return (T)_plugins?.FirstOrDefault(x => x.Name == name && x is T); } - public IEnumerable GetPlugins() + /// + public ICollection GetPlugins() { - return _plugins?.OfType() ?? new List(); + return _plugins?.OfType().ToArray(); } - public IEnumerable GetAllPlugins() + /// + public ICollection GetAllPlugins() { - return _plugins ?? new List(); + return _plugins; } + /// public void ReloadPlugins() { string pluginFolder = _config.GetValue("plugins"); if (!Directory.Exists(pluginFolder)) Directory.CreateDirectory(pluginFolder); - string[] pluginsPaths = Directory.GetFiles(pluginFolder); - _plugins = pluginsPaths.SelectMany(path => + _logger.LogTrace("Loading new plugins..."); + string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories); + ICollection newPlugins = pluginsPaths.SelectMany(path => { path = Path.GetFullPath(path); try { PluginDependencyLoader loader = new(path); - Assembly ass = loader.LoadFromAssemblyPath(path); - return ass.GetTypes() + Assembly assembly = loader.LoadFromAssemblyPath(path); + return assembly.GetTypes() .Where(x => typeof(IPlugin).IsAssignableFrom(x)) - .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)); + .Where(x => _plugins.All(y => y.GetType() != x)) + .Select(x => (IPlugin)_container.Resolve(x)); } catch (Exception ex) { - Console.Error.WriteLine($"\nError loading the plugin at {path}.\n{ex.GetType().Name}: {ex.Message}\n"); + _logger.LogError(ex, "Could not load the plugin at {Path}", path); return Array.Empty(); } - }).ToList(); - - if (!_plugins.Any()) + }).ToArray(); + _plugins.AddRange(newPlugins); + + ICollection available = _plugins.SelectMany(x => x.Provides).ToArray(); + foreach (IPlugin plugin in newPlugins) { - Console.WriteLine("\nNo plugin enabled.\n"); - return; + Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); + if (missing != null) + throw new MissingDependencyException(plugin.Name, missing.Name); + plugin.Configure(_container); } - Console.WriteLine("\nPlugin enabled:"); - foreach (IPlugin plugin in _plugins) - Console.WriteLine($"\t{plugin.Name}"); - Console.WriteLine(); + if (!_plugins.Any()) + _logger.LogInformation("No plugin enabled"); + else + _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); + } + + + /// + /// A custom to load plugin's dependency if they are on the same folder. + /// + private class PluginDependencyLoader : AssemblyLoadContext + { + /// + /// The basic resolver that will be used to load dlls. + /// + private readonly AssemblyDependencyResolver _resolver; + + /// + /// Create a new for the given path. + /// + /// The path of the plugin and it's dependencies + public PluginDependencyLoader(string pluginPath) + { + _resolver = new AssemblyDependencyResolver(pluginPath); + } + + /// + protected override Assembly Load(AssemblyName assemblyName) + { + string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + return LoadFromAssemblyPath(assemblyPath); + return base.Load(assemblyName); + } + + /// + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + return LoadUnmanagedDllFromPath(libraryPath); + return base.LoadUnmanagedDll(unmanagedDllName); + } } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 1af6c485..75583392 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -108,7 +108,7 @@ namespace Kyoo.Controllers { Episode ret = await GetOrDefault(showID, seasonNumber, episodeNumber); if (ret == null) - throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}."); + throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}."); return ret; } @@ -117,7 +117,7 @@ namespace Kyoo.Controllers { Episode ret = await GetOrDefault(showSlug, seasonNumber, episodeNumber); if (ret == null) - throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}."); + throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}."); return ret; } diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index c1db8e46..05a9f6ab 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -154,7 +154,7 @@ namespace Kyoo.Controllers sort, limit); if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); return items; } @@ -169,7 +169,7 @@ namespace Kyoo.Controllers sort, limit); if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); return items; } } diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 6de72a4b..a3b20e38 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -131,7 +131,7 @@ namespace Kyoo.Controllers sort, limit); if (!people.Any() && await _shows.Value.Get(showID) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -153,7 +153,7 @@ namespace Kyoo.Controllers sort, limit); if (!people.Any() && await _shows.Value.Get(showSlug) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -174,7 +174,7 @@ namespace Kyoo.Controllers sort, limit); if (!roles.Any() && await Get(id) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); return roles; } @@ -193,7 +193,7 @@ namespace Kyoo.Controllers sort, limit); if (!roles.Any() && await Get(slug) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); return roles; } } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index b0a61c90..e86a812b 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -88,7 +88,7 @@ namespace Kyoo.Controllers { Season ret = await GetOrDefault(showID, seasonNumber); if (ret == null) - throw new ItemNotFound($"No season {seasonNumber} found for the show {showID}"); + throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}"); ret.ShowSlug = await _shows.GetSlug(showID); return ret; } @@ -98,7 +98,7 @@ namespace Kyoo.Controllers { Season ret = await GetOrDefault(showSlug, seasonNumber); if (ret == null) - throw new ItemNotFound($"No season {seasonNumber} found for the show {showSlug}"); + throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}"); ret.ShowSlug = showSlug; return ret; } diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 55ddb427..4ce6da29 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -46,7 +46,7 @@ namespace Kyoo.Controllers { Track ret = await GetOrDefault(slug, type); if (ret == null) - throw new ItemNotFound($"No track found with the slug {slug} and the type {type}."); + throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}."); return ret; } diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index a625dcf2..6f9c184b 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -185,7 +185,7 @@ namespace Kyoo.Controllers int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); if (index == -1) - throw new ItemNotFound($"No task found with the slug {taskSlug}"); + throw new ItemNotFoundException($"No task found with the slug {taskSlug}"); _queuedTasks.Enqueue((_tasks[index].task, arguments)); _tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug)); } diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index 26e12c80..ac8b6fbe 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -1,7 +1,6 @@ +using System; using Kyoo.Controllers; using Kyoo.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Unity; using Unity.Lifetime; @@ -22,42 +21,40 @@ namespace Kyoo public string Description => "The core module containing default implementations."; /// - public string[] Provides => new[] + public Type[] Provides => new[] { - $"{nameof(IFileManager)}:file", - $"{nameof(ITranscoder)}:{nameof(Transcoder)}", - $"{nameof(IThumbnailsManager)}:{nameof(ThumbnailsManager)}", - $"{nameof(IProviderManager)}:{nameof(ProviderManager)}", - $"{nameof(IPluginManager)}:{nameof(PluginManager)}", - $"{nameof(ITaskManager)}:{nameof(TaskManager)}", - $"{nameof(ILibraryManager)}:{nameof(LibraryManager)}", - $"{nameof(ILibraryRepository)}:{nameof(LibraryRepository)}", - $"{nameof(ILibraryItemRepository)}:{nameof(LibraryItemRepository)}", - $"{nameof(ICollectionRepository)}:{nameof(CollectionRepository)}", - $"{nameof(IShowRepository)}:{nameof(ShowRepository)}", - $"{nameof(ISeasonRepository)}:{nameof(SeasonRepository)}", - $"{nameof(IEpisodeRepository)}:{nameof(EpisodeRepository)}", - $"{nameof(ITrackRepository)}:{nameof(TrackRepository)}", - $"{nameof(IPeopleRepository)}:{nameof(PeopleRepository)}", - $"{nameof(IStudioRepository)}:{nameof(StudioRepository)}", - $"{nameof(IGenreRepository)}:{nameof(GenreRepository)}", - $"{nameof(IProviderRepository)}:{nameof(ProviderRepository)}" + typeof(FileManager), + typeof(Transcoder), + typeof(ThumbnailsManager), + typeof(ProviderManager), + typeof(TaskManager), + typeof(LibraryManager), + typeof(LibraryRepository), + typeof(LibraryItemRepository), + typeof(CollectionRepository), + typeof(ShowRepository), + typeof(SeasonRepository), + typeof(EpisodeRepository), + typeof(TrackRepository), + typeof(PeopleRepository), + typeof(StudioRepository), + typeof(GenreRepository), + typeof(ProviderRepository), }; /// - public string[] Requires => new[] + public Type[] Requires => new[] { - "DatabaseContext:" + typeof(DatabaseContext) }; /// - public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) + public void Configure(IUnityContainer container) { container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); - container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new HierarchicalLifetimeManager()); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 92c27dba..740ae0c8 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -5,9 +5,9 @@ true Latest false - ../Kyoo.WebApp/ - ../Kyoo.WebLogin/ - ../Kyoo.Transcoder/ + $(SolutionDir)/Kyoo.WebApp/ + $(SolutionDir)/Kyoo.WebLogin/ + $(SolutionDir)/Kyoo.Transcoder/ $(DefaultItemExcludes);$(SpaRoot)node_modules/** diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index ee13bcdb..1dda599d 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -66,7 +66,7 @@ namespace Kyoo } /// - /// Createa a web host + /// Create a a web host /// /// Command line parameters that can be handled by kestrel /// A new web host instance diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index e5493f92..3a25f1d6 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -61,16 +61,9 @@ namespace Kyoo }); services.AddHttpClient(); - services.AddDbContext(options => - { - options.UseNpgsql(_configuration.GetDatabaseConnection()); - // .EnableSensitiveDataLogging() - // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); - }, ServiceLifetime.Transient); - services.AddDbContext(options => { - options.UseNpgsql(_configuration.GetDatabaseConnection()); + options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); }); string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; @@ -94,13 +87,13 @@ namespace Kyoo .AddConfigurationStore(options => { options.ConfigureDbContext = builder => - builder.UseNpgsql(_configuration.GetDatabaseConnection(), + builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), sql => sql.MigrationsAssembly(assemblyName)); }) .AddOperationalStore(options => { options.ConfigureDbContext = builder => - builder.UseNpgsql(_configuration.GetDatabaseConnection(), + builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), sql => sql.MigrationsAssembly(assemblyName)); options.EnableTokenCleanup = true; }) @@ -147,9 +140,6 @@ namespace Kyoo { AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } }); - - - services.AddScoped(); } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) @@ -214,11 +204,14 @@ namespace Kyoo if (env.IsDevelopment()) spa.UseAngularCliServer("start"); }); + + container.RegisterType(new SingletonLifetimeManager()); + IPluginManager pluginManager = new PluginManager(container, _configuration, new Logger(_loggerFactory)); + pluginManager.ReloadPlugins(); - new CoreModule().Configure(container, _configuration, app, env.IsDevelopment()); - container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); // TODO the reload should re inject components from the constructor. // TODO fin a way to inject tasks without a IUnityContainer. + container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); } } } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 7fc5699c..32ea147c 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -177,7 +177,7 @@ namespace Kyoo.Tasks await libraryManager.Create(track); Console.WriteLine($"Registering subtitle at: {path}."); } - catch (ItemNotFound) + catch (ItemNotFoundException) { await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}."); } @@ -317,7 +317,7 @@ namespace Kyoo.Tasks season.Show = show; return season; } - catch (ItemNotFound) + catch (ItemNotFoundException) { Season season = await MetadataProvider.GetSeason(show, seasonNumber, library); await libraryManager.CreateIfNotExists(season); diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index 8dd28956..a77f408e 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -68,7 +68,7 @@ namespace Kyoo.Api { return await _libraryManager.Get(showSlug, seasonNumber); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -82,7 +82,7 @@ namespace Kyoo.Api { return await _libraryManager.Get(showID, seasonNumber); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -183,7 +183,7 @@ namespace Kyoo.Api Episode episode = await _libraryManager.Get(id); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -198,7 +198,7 @@ namespace Kyoo.Api Episode episode = await _libraryManager.Get(slug); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/LibraryItemApi.cs b/Kyoo/Views/LibraryItemApi.cs index 9f97a275..5c36cbc2 100644 --- a/Kyoo/Views/LibraryItemApi.cs +++ b/Kyoo/Views/LibraryItemApi.cs @@ -47,7 +47,7 @@ namespace Kyoo.Api Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), limit); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/PeopleApi.cs b/Kyoo/Views/PeopleApi.cs index 8f80b6f2..ed68dea3 100644 --- a/Kyoo/Views/PeopleApi.cs +++ b/Kyoo/Views/PeopleApi.cs @@ -48,7 +48,7 @@ namespace Kyoo.Api return Page(resources, limit); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -76,7 +76,7 @@ namespace Kyoo.Api return Page(resources, limit); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index de623916..7b2a8868 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -247,7 +247,7 @@ namespace Kyoo.Api { return await _libraryManager.Get(x => x.Shows.Any(y => y.ID == showID)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -261,7 +261,7 @@ namespace Kyoo.Api { return await _libraryManager.Get(x => x.Shows.Any(y => y.Slug == slug)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -384,7 +384,7 @@ namespace Kyoo.Api .ToDictionary(Path.GetFileNameWithoutExtension, x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -401,7 +401,7 @@ namespace Kyoo.Api string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); return _files.FileResult(path); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -416,7 +416,7 @@ namespace Kyoo.Api Show show = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetShowPoster(show)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -431,7 +431,7 @@ namespace Kyoo.Api Show show = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetShowLogo(show)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -446,7 +446,7 @@ namespace Kyoo.Api Show show = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetShowBackdrop(show)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/TaskApi.cs b/Kyoo/Views/TaskApi.cs index 56b37b3d..1efb3cad 100644 --- a/Kyoo/Views/TaskApi.cs +++ b/Kyoo/Views/TaskApi.cs @@ -36,7 +36,7 @@ namespace Kyoo.Api _taskManager.StartTask(taskSlug, args); return Ok(); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/TrackApi.cs b/Kyoo/Views/TrackApi.cs index 79bced62..77125bdc 100644 --- a/Kyoo/Views/TrackApi.cs +++ b/Kyoo/Views/TrackApi.cs @@ -31,7 +31,7 @@ namespace Kyoo.Api { return await _libraryManager.Get(x => x.Tracks.Any(y => y.ID == id)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -47,7 +47,7 @@ namespace Kyoo.Api // TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor return await _libraryManager.Get(x => x.Tracks.Any(y => y.Slug == slug)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/VideoApi.cs b/Kyoo/Views/VideoApi.cs index 13c53e40..498a93a2 100644 --- a/Kyoo/Views/VideoApi.cs +++ b/Kyoo/Views/VideoApi.cs @@ -52,7 +52,7 @@ namespace Kyoo.Api Episode episode = await _libraryManager.Get(slug); return _files.FileResult(episode.Path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -71,7 +71,7 @@ namespace Kyoo.Api return StatusCode(500); return _files.FileResult(path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -90,7 +90,7 @@ namespace Kyoo.Api return StatusCode(500); return _files.FileResult(path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/Views/WatchApi.cs b/Kyoo/Views/WatchApi.cs index cd9327ae..7418a6ae 100644 --- a/Kyoo/Views/WatchApi.cs +++ b/Kyoo/Views/WatchApi.cs @@ -27,7 +27,7 @@ namespace Kyoo.Api Episode item = await _libraryManager.Get(slug); return await WatchItem.FromEpisode(item, _libraryManager); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index ef2aeb46..1622fe15 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -3,14 +3,16 @@ "public_url": "http://localhost:5000/", "database": { - "server": "127.0.0.1", - "port": "5432", - "database": "kyooDB", - "user ID": "kyoo", - "password": "kyooPassword", - "pooling": "true", - "maxPoolSize": "95", - "timeout": "30" + "postgres": { + "server": "127.0.0.1", + "port": "5432", + "database": "kyooDB", + "user ID": "kyoo", + "password": "kyooPassword", + "pooling": "true", + "maxPoolSize": "95", + "timeout": "30" + } }, "logging": { From 0453e74b672132864e59c415ae794f7dcbcce663 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 30 Apr 2021 01:13:22 +0200 Subject: [PATCH 05/25] Fixing build order --- Kyoo.Common/Kyoo.Common.csproj | 8 +------- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 6 ++---- Kyoo/Kyoo.csproj | 10 +++++----- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index b9417da8..dcd6610c 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -21,17 +21,11 @@ - + - - - ..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\5.0.5\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - - diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 5e4d8678..b870c769 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -2,9 +2,10 @@ net5.0 - $(SolutionDir)/Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql false false + false false SDG @@ -14,13 +15,10 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 740ae0c8..40a16662 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -5,9 +5,9 @@ true Latest false - $(SolutionDir)/Kyoo.WebApp/ - $(SolutionDir)/Kyoo.WebLogin/ - $(SolutionDir)/Kyoo.Transcoder/ + ../Kyoo.WebApp/ + ../Kyoo.WebLogin/ + ../Kyoo.Transcoder/ $(DefaultItemExcludes);$(SpaRoot)node_modules/** @@ -33,8 +33,6 @@ - - @@ -60,6 +58,8 @@ + + From c0f1f80a40e88e5fbb431a0396efa067770f09c0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 30 Apr 2021 02:02:11 +0200 Subject: [PATCH 06/25] Fixing module loading --- Kyoo.Common/Controllers/IPlugin.cs | 6 ++++++ .../Exceptions/MissingDependencyException.cs | 2 +- Kyoo.Postgresql/PostgresModule.cs | 3 +++ Kyoo/Controllers/PluginManager.cs | 17 +++++++++++++---- Kyoo/CoreModule.cs | 3 +++ Kyoo/Program.cs | 11 +++++++++-- Kyoo/Startup.cs | 9 ++++++++- 7 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index c1652a4a..56797bb2 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -46,6 +46,12 @@ namespace Kyoo.Controllers /// put typeof(ILibraryManager). /// Type[] Requires { get; } + + /// + /// True if this plugin is needed to start Kyoo. If this is true and a dependency could not be met, app startup + /// will be canceled. If this is false, Kyoo's startup will continue without enabling this plugin. + /// + bool IsRequired { get; } /// /// A configure method that will be run on plugin's startup. diff --git a/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs b/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs index ec459faf..b2a4aa43 100644 --- a/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs +++ b/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs @@ -14,7 +14,7 @@ namespace Kyoo.Models.Exceptions /// The name of the plugin that can't be loaded. /// The name of the missing dependency. public MissingDependencyException(string plugin, string dependency) - : base($"No {dependency} are available in kyoo but the plugin {plugin} requires it.") + : base($"No {dependency} are available in Kyoo but the plugin {plugin} requires it.") {} } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index c83df51d..e54f0a45 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -29,6 +29,9 @@ namespace Kyoo.Postgresql /// public Type[] Requires => Array.Empty(); + + /// + public bool IsRequired => true; /// diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 8b9e9ca2..d2945b0c 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -88,14 +88,17 @@ namespace Kyoo.Controllers return assembly.GetTypes() .Where(x => typeof(IPlugin).IsAssignableFrom(x)) .Where(x => _plugins.All(y => y.GetType() != x)) - .Select(x => (IPlugin)_container.Resolve(x)); + .Select(x => (IPlugin)_container.Resolve(x)) + .ToArray(); } catch (Exception ex) { _logger.LogError(ex, "Could not load the plugin at {Path}", path); return Array.Empty(); } - }).ToArray(); + }).ToList(); + if (!_plugins.Any()) + newPlugins.Add(new CoreModule()); _plugins.AddRange(newPlugins); ICollection available = _plugins.SelectMany(x => x.Provides).ToArray(); @@ -103,8 +106,14 @@ namespace Kyoo.Controllers { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); if (missing != null) - throw new MissingDependencyException(plugin.Name, missing.Name); - plugin.Configure(_container); + { + Exception error = new MissingDependencyException(plugin.Name, missing.Name); + _logger.LogCritical(error, "A plugin's dependency could not be met"); + if (plugin.IsRequired) + Environment.Exit(1); + } + else + plugin.Configure(_container); } if (!_plugins.Any()) diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index ac8b6fbe..5916f33f 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -48,6 +48,9 @@ namespace Kyoo typeof(DatabaseContext) }; + /// + public bool IsRequired => true; + /// public void Configure(IUnityContainer container) { diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 1dda599d..e5053735 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -49,7 +49,14 @@ namespace Kyoo IWebHostBuilder host = CreateWebHostBuilder(args); if (debug != null) host = host.UseEnvironment(debug == true ? "Development" : "Production"); - await host.Build().RunAsync(); + try + { + await host.Build().RunAsync(); + } + catch (Exception ex) + { + new Logger(new LoggerFactory()).LogCritical(ex, "Unhandled exception"); + } } /// @@ -74,7 +81,7 @@ namespace Kyoo { UnityContainer container = new(); container.EnableDebugDiagnostic(); - + return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build()) diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 3a25f1d6..0fc391d1 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -44,6 +44,8 @@ namespace Kyoo { string publicUrl = _configuration.GetValue("public_url"); + services.AddMvc().AddControllersAsServices(); + services.AddSpaStaticFiles(configuration => { configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot"); @@ -141,6 +143,11 @@ namespace Kyoo AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } }); } + + public void ConfigureContainer(IUnityContainer container) + { + // TODO move this to the configure section and figure out a way to reload ControllerActivators with the updated unity container + } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) { @@ -204,7 +211,7 @@ namespace Kyoo if (env.IsDevelopment()) spa.UseAngularCliServer("start"); }); - + container.RegisterType(new SingletonLifetimeManager()); IPluginManager pluginManager = new PluginManager(container, _configuration, new Logger(_loggerFactory)); pluginManager.ReloadPlugins(); From 36220613ab6bd2d654f0820ba2eb06e90b68ee98 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 1 May 2021 02:30:57 +0200 Subject: [PATCH 07/25] Fixing the unity container --- Kyoo.Common/Controllers/IPlugin.cs | 3 +- .../Implementations/LibraryManager.cs | 4 +- Kyoo.Common/Module.cs | 41 ++++++++++++++ Kyoo.Common/Utility.cs | 14 +++++ Kyoo.CommonAPI/DatabaseContext.cs | 23 ++------ Kyoo.Postgresql/PostgresContext.cs | 1 + Kyoo/Controllers/TaskManager.cs | 5 ++ Kyoo/CoreModule.cs | 56 +++++++++---------- Kyoo/Program.cs | 18 +++--- Kyoo/Startup.cs | 16 ++++-- Kyoo/UnityExtensions/UnityExtensions.cs | 32 +++++++++++ Kyoo/UnityExtensions/UnityProvider.cs | 30 ++++++++++ 12 files changed, 179 insertions(+), 64 deletions(-) create mode 100644 Kyoo/UnityExtensions/UnityExtensions.cs create mode 100644 Kyoo/UnityExtensions/UnityProvider.cs diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index 56797bb2..a2f9d1b0 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -32,8 +32,7 @@ namespace Kyoo.Controllers /// A list of services that are provided by this service. This allow other plugins to declare dependencies. /// /// - /// You should put directly the type that you will register in configure, Kyoo will detect by itself which - /// interfaces are implemented by your type. + /// You should put the type's interface that will be register in configure. /// Type[] Provides { get; } diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index dcf35813..ce34f267 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -40,7 +40,7 @@ namespace Kyoo.Controllers /// - /// Create a new instancce with every repository available. + /// Create a new instance with every repository available. /// /// The list of repositories that this library manager should manage. /// If a repository for every base type is not available, this instance won't be stable. @@ -66,7 +66,7 @@ namespace Kyoo.Controllers { if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository ret) return ret; - throw new ItemNotFoundException(); + throw new ItemNotFoundException($"No repository found for the type {typeof(T).Name}."); } /// diff --git a/Kyoo.Common/Module.cs b/Kyoo.Common/Module.cs index 639e7c2a..5ce34cbe 100644 --- a/Kyoo.Common/Module.cs +++ b/Kyoo.Common/Module.cs @@ -21,5 +21,46 @@ namespace Kyoo container.RegisterType(); return container; } + + /// + /// Register a new repository to the container. + /// + /// The container + /// The type of the repository. + /// + /// If your repository implements a special interface, please use + /// + /// The initial container. + public static IUnityContainer RegisterRepository(this IUnityContainer container) + where T : IBaseRepository + { + Type repository = Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)); + + if (repository != null) + { + container.RegisterType(repository, typeof(T)); + container.RegisterType(repository.FriendlyName()); + } + else + container.RegisterType(typeof(T).FriendlyName()); + return container; + } + + /// + /// Register a new repository with a custom mapping to the container. + /// + /// + /// The custom mapping you have for your repository. + /// The type of the repository. + /// + /// If your repository does not implements a special interface, please use + /// + /// The initial container. + public static IUnityContainer RegisterRepository(this IUnityContainer container) + where T2 : IBaseRepository, T + { + container.RegisterType(); + return container.RegisterRepository(); + } } } \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 3e4bba15..2469afbc 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -777,5 +777,19 @@ namespace Kyoo return true; return firstID == secondID; } + + /// + /// Get a friendly type name (supporting generics) + /// For example a list of string will be displayed as List<string> and not as List`1. + /// + /// The type to use + /// The friendly name of the type + public static string FriendlyName(this Type type) + { + if (!type.IsGenericType) + return type.Name; + string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); + return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; + } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index caf8933e..997b7bd4 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -82,28 +82,17 @@ namespace Kyoo { return Set>(); } - - - /// - /// A basic constructor that set default values (query tracker behaviors, mapping enums...) - /// - public DatabaseContext() - { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; - } /// - /// Create a new . + /// Set basic configurations (like preventing query tracking) /// - /// Connection options to use (witch database provider to use, connection strings...) - public DatabaseContext(DbContextOptions options) - : base(options) + /// An option builder to fill. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } - + /// /// Set database parameters to support every types of Kyoo. /// diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 7ee49adb..826b88e5 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -51,6 +51,7 @@ namespace Kyoo.Postgresql if (_debugMode) optionsBuilder.EnableDetailedErrors() .EnableSensitiveDataLogging(); + base.OnConfiguring(optionsBuilder); } /// diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 6f9c184b..89cff352 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -66,6 +66,11 @@ namespace Kyoo.Controllers _configuration = configuration.GetSection("scheduledTasks"); _logger = logger; _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + + if (_tasks.Any()) + _logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.task.Name)); + else + _logger.LogInformation("Task manager initiated without any tasks"); } diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index 5916f33f..a32689a5 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -23,23 +23,23 @@ namespace Kyoo /// public Type[] Provides => new[] { - typeof(FileManager), - typeof(Transcoder), - typeof(ThumbnailsManager), - typeof(ProviderManager), - typeof(TaskManager), - typeof(LibraryManager), - typeof(LibraryRepository), - typeof(LibraryItemRepository), - typeof(CollectionRepository), - typeof(ShowRepository), - typeof(SeasonRepository), - typeof(EpisodeRepository), - typeof(TrackRepository), - typeof(PeopleRepository), - typeof(StudioRepository), - typeof(GenreRepository), - typeof(ProviderRepository), + typeof(IFileManager), + typeof(ITranscoder), + typeof(IThumbnailsManager), + typeof(IProviderManager), + typeof(ITaskManager), + typeof(ILibraryManager), + typeof(ILibraryRepository), + typeof(ILibraryItemRepository), + typeof(ICollectionRepository), + typeof(IShowRepository), + typeof(ISeasonRepository), + typeof(IEpisodeRepository), + typeof(ITrackRepository), + typeof(IPeopleRepository), + typeof(IStudioRepository), + typeof(IGenreRepository), + typeof(IProviderRepository) }; /// @@ -62,17 +62,17 @@ namespace Kyoo container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); - container.RegisterType(new HierarchicalLifetimeManager()); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); container.RegisterTask(); } diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index e5053735..4d749a0f 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using Kyoo.UnityExtensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.Extensions.Configuration; @@ -8,7 +9,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Unity; -using Unity.Microsoft.DependencyInjection; namespace Kyoo { @@ -46,16 +46,16 @@ namespace Kyoo #endif Console.WriteLine($"Running as {Environment.UserName}."); - IWebHostBuilder host = CreateWebHostBuilder(args); + IWebHostBuilder builder = CreateWebHostBuilder(args); if (debug != null) - host = host.UseEnvironment(debug == true ? "Development" : "Production"); + builder = builder.UseEnvironment(debug == true ? "Development" : "Production"); try { - await host.Build().RunAsync(); + await builder.Build().RunAsync(); } catch (Exception ex) { - new Logger(new LoggerFactory()).LogCritical(ex, "Unhandled exception"); + await Console.Error.WriteLineAsync($"Unhandled exception: {ex}"); } } @@ -71,7 +71,7 @@ namespace Kyoo .AddEnvironmentVariables() .AddCommandLine(args); } - + /// /// Create a a web host /// @@ -80,8 +80,8 @@ namespace Kyoo private static IWebHostBuilder CreateWebHostBuilder(string[] args) { UnityContainer container = new(); - container.EnableDebugDiagnostic(); - + // container.EnableDebugDiagnostic(); + return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build()) @@ -99,7 +99,7 @@ namespace Kyoo if (context.HostingEnvironment.IsDevelopment()) StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); }) - .UseUnityServiceProvider(container) + .UseUnityProvider(container) .ConfigureServices(x => x.AddRouting()) .UseKestrel(options => { options.AddServerHeader = false; }) .UseIIS() diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 0fc391d1..1bdf23b2 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -142,11 +142,18 @@ namespace Kyoo { AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } }); + + services.AddSingleton(); + services.AddHostedService(x => x.GetService() as TaskManager); } - public void ConfigureContainer(IUnityContainer container) + public void ConfigureContainer(UnityContainer container) { // TODO move this to the configure section and figure out a way to reload ControllerActivators with the updated unity container + + // TODO the reload should re inject components from the constructor. + // TODO fin a way to inject tasks without a IUnityContainer. + // container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) @@ -213,12 +220,9 @@ namespace Kyoo }); container.RegisterType(new SingletonLifetimeManager()); - IPluginManager pluginManager = new PluginManager(container, _configuration, new Logger(_loggerFactory)); + // container.Resolve(); + IPluginManager pluginManager = container.Resolve(); pluginManager.ReloadPlugins(); - - // TODO the reload should re inject components from the constructor. - // TODO fin a way to inject tasks without a IUnityContainer. - container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); } } } diff --git a/Kyoo/UnityExtensions/UnityExtensions.cs b/Kyoo/UnityExtensions/UnityExtensions.cs new file mode 100644 index 00000000..b351ec8e --- /dev/null +++ b/Kyoo/UnityExtensions/UnityExtensions.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Unity; +using Unity.Microsoft.DependencyInjection; + +namespace Kyoo.UnityExtensions +{ + public static class UnityExtensions + { + public static IWebHostBuilder UseUnityProvider(this IWebHostBuilder host, UnityContainer container) + { + UnityProvider factory = new(container); + + return host.ConfigureServices((_, services) => + { + services.Replace(ServiceDescriptor.Singleton>(factory)); + services.Replace(ServiceDescriptor.Singleton>(factory)); + services.Replace(ServiceDescriptor.Singleton>(factory)); + }); + } + + public static IUnityContainer AddServices(this IUnityContainer container, IServiceCollection services) + { + return (IUnityContainer)typeof(ServiceProviderExtensions).Assembly + .GetType("Unity.Microsoft.DependencyInjection.Configuration") + !.GetMethod("AddServices", BindingFlags.Static | BindingFlags.NonPublic) + !.Invoke(null, new object[] {container, services}); + } + } +} \ No newline at end of file diff --git a/Kyoo/UnityExtensions/UnityProvider.cs b/Kyoo/UnityExtensions/UnityProvider.cs new file mode 100644 index 00000000..93d460f0 --- /dev/null +++ b/Kyoo/UnityExtensions/UnityProvider.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Unity; +using Unity.Microsoft.DependencyInjection; + +namespace Kyoo.UnityExtensions +{ + public class UnityProvider : ServiceProviderFactory, IServiceProviderFactory + { + private readonly UnityContainer _container; + + + public UnityProvider(UnityContainer container) + : base(container) + { + _container = container; + } + + public UnityContainer CreateBuilder(IServiceCollection services) + { + _container.AddServices(services); + return _container; + } + + public IServiceProvider CreateServiceProvider(UnityContainer containerBuilder) + { + return CreateServiceProvider(containerBuilder as IUnityContainer); + } + } +} \ No newline at end of file From 97511d59884fed3049f7b6656df89dd939c7fbe9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 1 May 2021 17:55:29 +0200 Subject: [PATCH 08/25] Suporting optional dependencies --- Kyoo.Authentication/Class1.cs | 6 + .../Kyoo.Authentication.csproj | 23 +++ Kyoo.Common/Controllers/IPlugin.cs | 168 ++++++++++++++++-- Kyoo.Postgresql/PostgresModule.cs | 11 +- Kyoo.sln | 6 + Kyoo/Controllers/PluginManager.cs | 46 ++++- Kyoo/CoreModule.cs | 67 +++---- Kyoo/Program.cs | 2 +- Kyoo/Startup.cs | 9 +- 9 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 Kyoo.Authentication/Class1.cs create mode 100644 Kyoo.Authentication/Kyoo.Authentication.csproj diff --git a/Kyoo.Authentication/Class1.cs b/Kyoo.Authentication/Class1.cs new file mode 100644 index 00000000..18aefd85 --- /dev/null +++ b/Kyoo.Authentication/Class1.cs @@ -0,0 +1,6 @@ +using System; + +namespace Kyoo.Authentication +{ + public class Class1 { } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj new file mode 100644 index 00000000..75bd3077 --- /dev/null +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + false + false + false + false + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + + + + + all + false + + + diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index a2f9d1b0..9f0e68e0 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Unity; @@ -29,34 +31,38 @@ namespace Kyoo.Controllers string Description { get; } /// - /// A list of services that are provided by this service. This allow other plugins to declare dependencies. + /// A list of services that are provided by this service. This allow other plugins to declare dependencies /// /// /// You should put the type's interface that will be register in configure. /// - Type[] Provides { get; } + ICollection Provides { get; } /// - /// A list of services that are required by this service. - /// The Core will warn the user that this plugin can't be loaded if a required service is not found. + /// A list of types that will be provided only if a condition is met. The condition can be an arbitrary method or + /// a condition based on other type availability. For more information, see . + /// + ICollection ConditionalProvides { get; } + + /// + /// A list of services that are required by this plugin. + /// You can put services that you provide conditionally here if you want. + /// Kyoo will warn the user that this plugin can't be loaded if a required service is not found. /// /// /// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager, /// put typeof(ILibraryManager). /// - Type[] Requires { get; } - - /// - /// True if this plugin is needed to start Kyoo. If this is true and a dependency could not be met, app startup - /// will be canceled. If this is false, Kyoo's startup will continue without enabling this plugin. - /// - bool IsRequired { get; } + ICollection Requires { get; } /// /// A configure method that will be run on plugin's startup. /// /// A unity container to register new services. - void Configure(IUnityContainer container); + /// The list of types that are available for this instance. This can be used + /// for conditional type. See + /// or > + void Configure(IUnityContainer container, ICollection availableTypes); /// /// An optional configuration step to allow a plugin to change asp net configurations. @@ -64,6 +70,144 @@ namespace Kyoo.Controllers /// /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities. void ConfigureAspNet(IApplicationBuilder app) {} + } + + /// + /// A type that will only be provided if a special condition is met. To check that your condition is met, + /// you can check the class. + /// + public class ConditionalProvide : Tuple + { + /// + /// Get the type that may be provided + /// + public Type Type => Item1; + + /// + /// Get the condition. + /// + public ProviderCondition Condition => Item2; + /// + /// Create a from a type and a condition. + /// + /// The type to provide + /// The condition + public ConditionalProvide(Type type, ProviderCondition condition) + : base(type, condition) + { } + + /// + /// Create a from a tuple of (Type, ProviderCondition). + /// + /// The tuple to convert + public ConditionalProvide((Type type, ProviderCondition condition) tuple) + : base(tuple.type, tuple.condition) + { } + + /// + /// Implicitly convert a tuple to a . + /// + /// The tuple to convert + /// A new based on the given tuple. + public static implicit operator ConditionalProvide((Type, Type) tuple) => new (tuple); + } + + /// + /// A condition for a conditional type. + /// + public class ProviderCondition + { + /// + /// The condition as a method. If true is returned, the type will be provided. + /// + public Func Condition { get; } = () => true; + /// + /// The list of types that this method needs. + /// + public ICollection Needed { get; } = ArraySegment.Empty; + + + /// + /// Create a new from a raw function. + /// + /// The predicate that will be used as condition + public ProviderCondition(Func condition) + { + Condition = condition; + } + + /// + /// Create a new from a type. This allow you to inform that a type will + /// only be available if a dependency is met. + /// + /// The type that you need + public ProviderCondition(Type needed) + { + Needed = new[] {needed}; + } + + /// + /// Create a new from a list of type. This allow you to inform that a type will + /// only be available if a list of dependencies are met. + /// + /// The types that you need + public ProviderCondition(ICollection needed) + { + Needed = needed; + } + + /// + /// Create a new with a list of types as dependencies and a predicate + /// for arbitrary conditions. + /// + /// The list of dependencies + /// An arbitrary condition + public ProviderCondition(ICollection needed, Func condition) + { + Needed = needed; + Condition = condition; + } + + + /// + /// Implicitly convert a type to a . + /// + /// The type dependency + /// A that will return true if the given type is available. + public static implicit operator ProviderCondition(Type type) => new(type); + + /// + /// Implicitly convert a list of type to a . + /// + /// The list of type dependencies + /// A that will return true if the given types are available. + public static implicit operator ProviderCondition(Type[] types) => new(types); + + /// + public static implicit operator ProviderCondition(List types) => new(types); + + + /// + /// Check if a type is available. + /// + /// The type to check + /// The list of types + /// True if the dependency is met, false otherwise + public static bool Has(Type needed, ICollection available) + { + return available.Contains(needed); + } + + /// + /// Check if a list of type are available. + /// + /// The list of types to check + /// The list of types + /// True if the dependencies are met, false otherwise + public static bool Has(ICollection needed, ICollection available) + { + return needed.All(x => Has(x, available)); + } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index e54f0a45..f3eecff0 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Kyoo.Controllers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -22,16 +23,16 @@ namespace Kyoo.Postgresql public string Description => "A database context for postgresql."; /// - public Type[] Provides => new[] + public ICollection Provides => new[] { typeof(PostgresContext) }; /// - public Type[] Requires => Array.Empty(); - + public ICollection ConditionalProvides => ArraySegment.Empty; + /// - public bool IsRequired => true; + public ICollection Requires => ArraySegment.Empty; /// @@ -56,7 +57,7 @@ namespace Kyoo.Postgresql } /// - public void Configure(IUnityContainer container) + public void Configure(IUnityContainer container, ICollection availableTypes) { container.RegisterFactory(_ => new PostgresContext( _configuration.GetDatabaseConnection("postgres"), diff --git a/Kyoo.sln b/Kyoo.sln index 79ee6e7d..3f814bd3 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Ky EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,5 +37,9 @@ Global {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = Release|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index d2945b0c..d2df582e 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -101,7 +101,7 @@ namespace Kyoo.Controllers newPlugins.Add(new CoreModule()); _plugins.AddRange(newPlugins); - ICollection available = _plugins.SelectMany(x => x.Provides).ToArray(); + ICollection available = GetProvidedTypes(); foreach (IPlugin plugin in newPlugins) { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); @@ -109,11 +109,9 @@ namespace Kyoo.Controllers { Exception error = new MissingDependencyException(plugin.Name, missing.Name); _logger.LogCritical(error, "A plugin's dependency could not be met"); - if (plugin.IsRequired) - Environment.Exit(1); } else - plugin.Configure(_container); + plugin.Configure(_container, available); } if (!_plugins.Any()) @@ -122,6 +120,46 @@ namespace Kyoo.Controllers _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); } + /// + /// Get the list of types provided by the currently loaded plugins. + /// + /// The list of types available. + private ICollection GetProvidedTypes() + { + List available = _plugins.SelectMany(x => x.Provides).ToList(); + List conditionals =_plugins + .SelectMany(x => x.ConditionalProvides) + .Where(x => x.Condition.Condition()) + .ToList(); + + bool IsAvailable(ConditionalProvide conditional, bool log = false) + { + if (!conditional.Condition.Condition()) + return false; + + ICollection needed = conditional.Condition.Needed + .Where(y => !available.Contains(y)) + .ToList(); + needed = needed.Where(x => !conditionals + .Where(y => y.Type == x) + .Any(y => IsAvailable(y))) + .ToList(); + if (!needed.Any()) + return true; + _logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met", + conditional.Type.Name, + needed.Select(x => x.Name)); + return false; + } + + foreach (ConditionalProvide conditional in conditionals) + { + if (IsAvailable(conditional, true)) + available.Add(conditional.Type); + } + return available; + } + /// /// A custom to load plugin's dependency if they are on the same folder. diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index a32689a5..9f9810cf 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Kyoo.Controllers; using Kyoo.Tasks; using Unity; @@ -21,38 +22,37 @@ namespace Kyoo public string Description => "The core module containing default implementations."; /// - public Type[] Provides => new[] + public ICollection Provides => new[] { typeof(IFileManager), typeof(ITranscoder), typeof(IThumbnailsManager), typeof(IProviderManager), typeof(ITaskManager), - typeof(ILibraryManager), - typeof(ILibraryRepository), - typeof(ILibraryItemRepository), - typeof(ICollectionRepository), - typeof(IShowRepository), - typeof(ISeasonRepository), - typeof(IEpisodeRepository), - typeof(ITrackRepository), - typeof(IPeopleRepository), - typeof(IStudioRepository), - typeof(IGenreRepository), - typeof(IProviderRepository) + typeof(ILibraryManager) }; /// - public Type[] Requires => new[] + public ICollection ConditionalProvides => new ConditionalProvide[] { - typeof(DatabaseContext) + (typeof(ILibraryRepository), typeof(DatabaseContext)), + (typeof(ILibraryItemRepository), typeof(DatabaseContext)), + (typeof(ICollectionRepository), typeof(DatabaseContext)), + (typeof(IShowRepository), typeof(DatabaseContext)), + (typeof(ISeasonRepository), typeof(DatabaseContext)), + (typeof(IEpisodeRepository), typeof(DatabaseContext)), + (typeof(ITrackRepository), typeof(DatabaseContext)), + (typeof(IPeopleRepository), typeof(DatabaseContext)), + (typeof(IStudioRepository), typeof(DatabaseContext)), + (typeof(IGenreRepository), typeof(DatabaseContext)), + (typeof(IProviderRepository), typeof(DatabaseContext)) }; /// - public bool IsRequired => true; - - /// - public void Configure(IUnityContainer container) + public ICollection Requires => ArraySegment.Empty; + + /// + public void Configure(IUnityContainer container, ICollection availableTypes) { container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); @@ -61,19 +61,22 @@ namespace Kyoo container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new HierarchicalLifetimeManager()); - - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - + + if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes)) + { + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + } + container.RegisterTask(); } } diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 4d749a0f..beba3b0d 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -80,7 +80,7 @@ namespace Kyoo private static IWebHostBuilder CreateWebHostBuilder(string[] args) { UnityContainer container = new(); - // container.EnableDebugDiagnostic(); + container.EnableDebugDiagnostic(); return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 1bdf23b2..d747c960 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -147,14 +147,7 @@ namespace Kyoo services.AddHostedService(x => x.GetService() as TaskManager); } - public void ConfigureContainer(UnityContainer container) - { - // TODO move this to the configure section and figure out a way to reload ControllerActivators with the updated unity container - - // TODO the reload should re inject components from the constructor. - // TODO fin a way to inject tasks without a IUnityContainer. - // container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); - } + public void ConfigureContainer(UnityContainer container) { } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) { From 7613efb4b4aea6851d5f7e9f0d319aec1c46ea10 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 3 May 2021 01:48:53 +0200 Subject: [PATCH 09/25] Moving authentification beahviors to a specific module --- .../AccountApi.cs | 0 Kyoo.Authentication/AuthManager.cs | 37 + Kyoo.Authentication/AuthRequirement.cs | 14 + Kyoo.Authentication/AuthenticationModule.cs | 171 +++ Kyoo.Authentication/Certificates.cs | 120 +++ Kyoo.Authentication/Class1.cs | 6 - .../IdentityContext.cs | 2 +- Kyoo.Authentication/IdentityDatabase.cs | 47 + .../Kyoo.Authentication.csproj | 13 +- .../Models/CertificateOption.cs | 26 + {Kyoo/Models => Kyoo.Authentication}/User.cs | 0 Kyoo.Common/Controllers/IPlugin.cs | 21 +- Kyoo.Common/Kyoo.Common.csproj | 6 + Kyoo.Common/MethodOfUtils.cs | 91 ++ .../Models/Attributes/MergeAttributes.cs | 13 +- .../Models/Attributes/PermissionAttribute.cs | 23 + Kyoo.Common/Utility.cs | 8 +- Kyoo.CommonAPI/DatabaseContext.cs | 11 + Kyoo.Postgresql/PostgresContext.cs | 11 + Kyoo.Postgresql/PostgresModule.cs | 2 +- Kyoo/Controllers/AuthManager.cs | 137 --- Kyoo/Controllers/PluginManager.cs | 15 +- .../Repositories/CollectionRepository.cs | 2 +- .../Repositories/EpisodeRepository.cs | 3 +- .../Repositories/GenreRepository.cs | 2 +- .../Repositories/LibraryItemRepository.cs | 2 +- .../Repositories/LibraryRepository.cs | 2 +- .../Repositories/PeopleRepository.cs | 3 +- .../Repositories/ProviderRepository.cs | 2 +- .../Repositories/SeasonRepository.cs | 2 +- .../Repositories/ShowRepository.cs | 4 +- .../Repositories/StudioRepository.cs | 2 +- Kyoo/Kyoo.csproj | 11 - .../20210306161631_Initial.Designer.cs | 985 ------------------ .../20210306161631_Initial.cs | 658 ------------ .../ConfigurationDbContextModelSnapshot.cs | 983 ----------------- .../20210216205030_Initial.Designer.cs | 384 ------- .../IdentityDatbase/20210216205030_Initial.cs | 291 ------ .../IdentityDatabaseModelSnapshot.cs | 382 ------- Kyoo/Models/IdentityDatabase.cs | 47 - Kyoo/Startup.cs | 148 +-- Kyoo/Views/TaskApi.cs | 5 +- Kyoo/settings.json | 14 +- 43 files changed, 670 insertions(+), 4036 deletions(-) rename {Kyoo/Views => Kyoo.Authentication}/AccountApi.cs (100%) create mode 100644 Kyoo.Authentication/AuthManager.cs create mode 100644 Kyoo.Authentication/AuthRequirement.cs create mode 100644 Kyoo.Authentication/AuthenticationModule.cs create mode 100644 Kyoo.Authentication/Certificates.cs delete mode 100644 Kyoo.Authentication/Class1.cs rename {Kyoo/Models => Kyoo.Authentication}/IdentityContext.cs (98%) create mode 100644 Kyoo.Authentication/IdentityDatabase.cs create mode 100644 Kyoo.Authentication/Models/CertificateOption.cs rename {Kyoo/Models => Kyoo.Authentication}/User.cs (100%) create mode 100644 Kyoo.Common/MethodOfUtils.cs create mode 100644 Kyoo.Common/Models/Attributes/PermissionAttribute.cs delete mode 100644 Kyoo/Controllers/AuthManager.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs delete mode 100644 Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs delete mode 100644 Kyoo/Models/IdentityDatabase.cs diff --git a/Kyoo/Views/AccountApi.cs b/Kyoo.Authentication/AccountApi.cs similarity index 100% rename from Kyoo/Views/AccountApi.cs rename to Kyoo.Authentication/AccountApi.cs diff --git a/Kyoo.Authentication/AuthManager.cs b/Kyoo.Authentication/AuthManager.cs new file mode 100644 index 00000000..2002db63 --- /dev/null +++ b/Kyoo.Authentication/AuthManager.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityServer4.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Authentication +{ + public class AuthorizationValidatorHandler : AuthorizationHandler + { + private readonly IConfiguration _configuration; + + public AuthorizationValidatorHandler(IConfiguration configuration) + { + _configuration = configuration; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizationValidator requirement) + { + if (!context.User.IsAuthenticated()) + { + string defaultPerms = _configuration.GetValue("defaultPermissions"); + if (defaultPerms.Split(',').Contains(requirement.Permission.ToLower())) + context.Succeed(requirement); + } + else + { + Claim perms = context.User.Claims.FirstOrDefault(x => x.Type == "permissions"); + if (perms != null && perms.Value.Split(",").Contains(requirement.Permission.ToLower())) + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/AuthRequirement.cs b/Kyoo.Authentication/AuthRequirement.cs new file mode 100644 index 00000000..e27e5d65 --- /dev/null +++ b/Kyoo.Authentication/AuthRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Kyoo.Authentication +{ + public class AuthorizationValidator : IAuthorizationRequirement + { + public string Permission { get; } + + public AuthorizationValidator(string permission) + { + Permission = permission; + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs new file mode 100644 index 00000000..b9e575c5 --- /dev/null +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using IdentityServer4.Extensions; +using IdentityServer4.Services; +using Kyoo.Authentication.Models; +using Kyoo.Controllers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Kyoo.Authentication +{ + /// + /// A module that enable OpenID authentication for Kyoo. + /// + public class AuthenticationModule : IPlugin + { + /// + public string Slug => "auth"; + + /// + public string Name => "Authentication"; + + /// + public string Description => "Enable OpenID authentication for Kyoo."; + + /// + public ICollection Provides => ArraySegment.Empty; + + /// + public ICollection ConditionalProvides => ArraySegment.Empty; + + /// + public ICollection Requires => ArraySegment.Empty; + + + /// + /// The configuration to use. + /// + private readonly IConfiguration _configuration; + + /// + /// A logger factory to allow IdentityServer to log things. + /// + private readonly ILoggerFactory _loggerFactory; + + + /// + /// Create a new authentication module instance and use the given configuration and environment. + /// + /// The configuration to use + /// The logger factory to allow IdentityServer to log things + public AuthenticationModule(IConfiguration configuration, ILoggerFactory loggerFactory) + { + _configuration = configuration; + _loggerFactory = loggerFactory; + } + + /// + public IServiceCollection Configure(IServiceCollection services, ICollection availableTypes) + { + string publicUrl = _configuration.GetValue("public_url"); + + // services.AddDbContext(options => + // { + // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); + // }); + + // services.AddIdentityCore(o => + // { + // o.Stores.MaxLengthForKeys = 128; + // }) + // .AddSignInManager() + // .AddDefaultTokenProviders() + // .AddEntityFrameworkStores(); + + CertificateOption certificateOptions = new(); + _configuration.GetSection(CertificateOption.Path).Bind(certificateOptions); + services.AddIdentityServer(options => + { + options.IssuerUri = publicUrl; + options.UserInteraction.LoginUrl = publicUrl + "login"; + options.UserInteraction.ErrorUrl = publicUrl + "error"; + options.UserInteraction.LogoutUrl = publicUrl + "logout"; + }) + // .AddAspNetIdentity() + // .AddConfigurationStore(options => + // { + // options.ConfigureDbContext = builder => + // builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), + // sql => sql.MigrationsAssembly(assemblyName)); + // }) + // .AddOperationalStore(options => + // { + // options.ConfigureDbContext = builder => + // builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), + // sql => sql.MigrationsAssembly(assemblyName)); + // options.EnableTokenCleanup = true; + // }) + .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) + .AddInMemoryApiScopes(IdentityContext.GetScopes()) + .AddInMemoryApiResources(IdentityContext.GetApis()) + // .AddProfileService() + .AddSigninKeys(certificateOptions); + + services.AddAuthentication(o => + { + o.DefaultScheme = IdentityConstants.ApplicationScheme; + o.DefaultSignInScheme = IdentityConstants.ExternalScheme; + }) + .AddIdentityCookies(_ => { }); + services.AddAuthentication() + .AddJwtBearer(options => + { + options.Authority = publicUrl; + options.Audience = "Kyoo"; + options.RequireHttpsMetadata = false; + }); + + services.AddAuthorization(options => + { + AuthorizationPolicyBuilder scheme = new(IdentityConstants.ApplicationScheme, + JwtBearerDefaults.AuthenticationScheme); + options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build(); + + string[] permissions = {"Read", "Write", "Play", "Admin"}; + foreach (string permission in permissions) + { + options.AddPolicy(permission, policy => + { + policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme); + policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); + policy.AddRequirements(new AuthorizationValidator(permission)); + policy.RequireScope($"kyoo.{permission.ToLower()}"); + }); + } + }); + services.AddSingleton(); + + DefaultCorsPolicyService cors = new(_loggerFactory.CreateLogger()) + { + AllowedOrigins = {new Uri(publicUrl).GetLeftPart(UriPartial.Authority)} + }; + services.AddSingleton(cors); + return services; + } + + /// + public void ConfigureAspNet(IApplicationBuilder app) + { + app.UseAuthorization(); + + app.UseCookiePolicy(new CookiePolicyOptions + { + MinimumSameSitePolicy = SameSiteMode.Strict + }); + app.UseAuthentication(); + app.Use((ctx, next) => + { + ctx.SetIdentityServerOrigin(_configuration.GetValue("public_url")); + return next(); + }); + app.UseIdentityServer(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Certificates.cs b/Kyoo.Authentication/Certificates.cs new file mode 100644 index 00000000..3aaab9b8 --- /dev/null +++ b/Kyoo.Authentication/Certificates.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Kyoo.Authentication.Models; +using Microsoft.Extensions.DependencyInjection; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Operators; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.X509; +using X509Certificate = Org.BouncyCastle.X509.X509Certificate; + +namespace Kyoo.Authentication +{ + /// + /// A class containing multiple extensions methods to manage certificates. + /// + public static class Certificates + { + /// + /// Add the certificate file to the identity server. If the certificate will expire soon, automatically renew it. + /// If no certificate exists, one is generated. + /// + /// The identity server that will be modified. + /// The certificate options + /// + public static IIdentityServerBuilder AddSigninKeys(this IIdentityServerBuilder builder, + CertificateOption options) + { + X509Certificate2 certificate = GetCertificate(options); + builder.AddSigningCredential(certificate); + + if (certificate.NotAfter.AddDays(7) <= DateTime.UtcNow) + { + Console.WriteLine("Signin certificate will expire soon, renewing it."); + if (File.Exists(options.OldFile)) + File.Delete(options.OldFile); + File.Move(options.File, options.OldFile); + builder.AddValidationKey(GenerateCertificate(options.File, options.Password)); + } + else if (File.Exists(options.OldFile)) + builder.AddValidationKey(GetExistingCredential(options.OldFile, options.Password)); + return builder; + } + + /// + /// Get or generate the sign-in certificate. + /// + /// The certificate options + /// A valid certificate + private static X509Certificate2 GetCertificate(CertificateOption options) + { + return File.Exists(options.File) + ? GetExistingCredential(options.File, options.Password) + : GenerateCertificate(options.File, options.Password); + } + + /// + /// Load a certificate from a file + /// + /// The path of the certificate + /// The password of the certificate + /// The loaded certificate + private static X509Certificate2 GetExistingCredential(string file, string password) + { + return new(file, password, + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable + ); + } + + /// + /// Generate a new certificate key and put it in the file at . + /// + /// The path of the output file + /// The password of the new certificate + /// The generated certificate + private static X509Certificate2 GenerateCertificate(string file, string password) + { + SecureRandom random = new(); + + X509V3CertificateGenerator certificateGenerator = new(); + certificateGenerator.SetSerialNumber(BigIntegers.CreateRandomInRange(BigInteger.One, + BigInteger.ValueOf(long.MaxValue), random)); + certificateGenerator.SetIssuerDN(new X509Name($"C=NL, O=SDG, CN=Kyoo")); + certificateGenerator.SetSubjectDN(new X509Name($"C=NL, O=SDG, CN=Kyoo")); + certificateGenerator.SetNotBefore(DateTime.UtcNow.Date); + certificateGenerator.SetNotAfter(DateTime.UtcNow.Date.AddMonths(3)); + + KeyGenerationParameters keyGenerationParameters = new(random, 2048); + RsaKeyPairGenerator keyPairGenerator = new(); + keyPairGenerator.Init(keyGenerationParameters); + + AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair(); + certificateGenerator.SetPublicKey(subjectKeyPair.Public); + + const string signatureAlgorithm = "MD5WithRSA"; + Asn1SignatureFactory signatureFactory = new(signatureAlgorithm, subjectKeyPair.Private); + X509Certificate bouncyCert = certificateGenerator.Generate(signatureFactory); + + Pkcs12Store store = new Pkcs12StoreBuilder().Build(); + store.SetKeyEntry("Kyoo_key", new AsymmetricKeyEntry(subjectKeyPair.Private), new [] + { + new X509CertificateEntry(bouncyCert) + }); + + using MemoryStream pfxStream = new(); + store.Save(pfxStream, password.ToCharArray(), random); + X509Certificate2 certificate = new(pfxStream.ToArray(), password, X509KeyStorageFlags.Exportable); + using FileStream fileStream = File.OpenWrite(file); + pfxStream.WriteTo(fileStream); + return certificate; + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Class1.cs b/Kyoo.Authentication/Class1.cs deleted file mode 100644 index 18aefd85..00000000 --- a/Kyoo.Authentication/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace Kyoo.Authentication -{ - public class Class1 { } -} \ No newline at end of file diff --git a/Kyoo/Models/IdentityContext.cs b/Kyoo.Authentication/IdentityContext.cs similarity index 98% rename from Kyoo/Models/IdentityContext.cs rename to Kyoo.Authentication/IdentityContext.cs index c414efcc..21d649e1 100644 --- a/Kyoo/Models/IdentityContext.cs +++ b/Kyoo.Authentication/IdentityContext.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using IdentityServer4.Models; -namespace Kyoo +namespace Kyoo.Authentication { public static class IdentityContext { diff --git a/Kyoo.Authentication/IdentityDatabase.cs b/Kyoo.Authentication/IdentityDatabase.cs new file mode 100644 index 00000000..8b4f321e --- /dev/null +++ b/Kyoo.Authentication/IdentityDatabase.cs @@ -0,0 +1,47 @@ +// using System.Threading.Tasks; +// using IdentityServer4.EntityFramework.Entities; +// using IdentityServer4.EntityFramework.Extensions; +// using IdentityServer4.EntityFramework.Interfaces; +// using IdentityServer4.EntityFramework.Options; +// using Kyoo.Models; +// using Microsoft.AspNetCore.Identity; +// using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +// using Microsoft.EntityFrameworkCore; +// using Microsoft.Extensions.Options; +// +// namespace Kyoo +// { +// // The configuration's database is named ConfigurationDbContext. +// public class IdentityDatabase : IdentityDbContext, IPersistedGrantDbContext +// { +// private readonly IOptions _operationalStoreOptions; +// +// public IdentityDatabase(DbContextOptions options, IOptions operationalStoreOptions) +// : base(options) +// { +// _operationalStoreOptions = operationalStoreOptions; +// } +// +// public DbSet Accounts { get; set; } +// +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// base.OnModelCreating(modelBuilder); +// modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value); +// +// modelBuilder.Entity().ToTable("User"); +// modelBuilder.Entity>().ToTable("UserRole"); +// modelBuilder.Entity>().ToTable("UserLogin"); +// modelBuilder.Entity>().ToTable("UserClaim"); +// modelBuilder.Entity().ToTable("UserRoles"); +// modelBuilder.Entity>().ToTable("UserRoleClaim"); +// modelBuilder.Entity>().ToTable("UserToken"); +// } +// +// public Task SaveChangesAsync() => base.SaveChangesAsync(); +// +// public DbSet PersistedGrants { get; set; } +// public DbSet DeviceFlowCodes { get; set; } +// +// } +// } \ No newline at end of file diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj index 75bd3077..29c2a08a 100644 --- a/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -2,11 +2,12 @@ net5.0 - ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/authentication false false false false + true SDG Zoe Roux @@ -14,6 +15,16 @@ default + + + + + + + + + + all diff --git a/Kyoo.Authentication/Models/CertificateOption.cs b/Kyoo.Authentication/Models/CertificateOption.cs new file mode 100644 index 00000000..93d2a878 --- /dev/null +++ b/Kyoo.Authentication/Models/CertificateOption.cs @@ -0,0 +1,26 @@ +namespace Kyoo.Authentication.Models +{ + /// + /// A typed option model for the certificate + /// + public class CertificateOption + { + /// + /// The path to get this option from the root configuration. + /// + public const string Path = "authentication:certificate"; + + /// + /// The path of the certificate file. + /// + public string File { get; set; } + /// + /// The path of the old certificate file. + /// + public string OldFile { get; set; } + /// + /// The password of the certificates. + /// + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo/Models/User.cs b/Kyoo.Authentication/User.cs similarity index 100% rename from Kyoo/Models/User.cs rename to Kyoo.Authentication/User.cs diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index 9f0e68e0..e9776d36 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; using Unity; namespace Kyoo.Controllers @@ -62,7 +63,25 @@ namespace Kyoo.Controllers /// The list of types that are available for this instance. This can be used /// for conditional type. See /// or > - void Configure(IUnityContainer container, ICollection availableTypes); + void Configure(IUnityContainer container, ICollection availableTypes) {} + + /// + /// An optional configure method that will be run on plugin's startup. + /// This method may be used instead or with the + /// method + /// if you use a library that configure itself with a . + /// Every service put in this container will be registered to the unity container after this method. + /// + /// An empty service container to register new services. + /// The list of types that are available for this instance. This can be used + /// for conditional type. See + /// or > + /// You should return the parameter or another container if you want. + /// This container will be added to Kyoo's unity container. + IServiceCollection Configure(IServiceCollection services, ICollection availableTypes) + { + return services; + } /// /// An optional configuration step to allow a plugin to change asp net configurations. diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index dcd6610c..7211beef 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -28,4 +28,10 @@ + + + ..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\5.0.5\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + diff --git a/Kyoo.Common/MethodOfUtils.cs b/Kyoo.Common/MethodOfUtils.cs new file mode 100644 index 00000000..5d051f69 --- /dev/null +++ b/Kyoo.Common/MethodOfUtils.cs @@ -0,0 +1,91 @@ +using System; +using System.Reflection; + +namespace Kyoo +{ + /// + /// Static class containing MethodOf calls. + /// + public static class MethodOfUtils + { + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Action action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Action action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Action action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Action action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Func action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Func action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Func action) + { + return action.Method; + } + + /// + /// Get a MethodInfo from a direct method. + /// + /// The method (without any arguments or return value. + /// The of the given method + public static MethodInfo MethodOf(Func action) + { + return action.Method; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/MergeAttributes.cs b/Kyoo.Common/Models/Attributes/MergeAttributes.cs index 399f5389..54d49d52 100644 --- a/Kyoo.Common/Models/Attributes/MergeAttributes.cs +++ b/Kyoo.Common/Models/Attributes/MergeAttributes.cs @@ -2,10 +2,21 @@ using System; namespace Kyoo.Models.Attributes { - public class NotMergableAttribute : Attribute { } + /// + /// Specify that a property can't be merged. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NotMergeableAttribute : Attribute { } + /// + /// An interface with a method called when this object is merged. + /// public interface IOnMerge { + /// + /// This function is called after the object has been merged. + /// + /// The object that has been merged with this. void OnMerge(object merged); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/PermissionAttribute.cs b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs new file mode 100644 index 00000000..fd2285e9 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + /// + /// Specify permissions needed for the API. + /// + [AttributeUsage(AttributeTargets.Method)] + public class PermissionAttribute : Attribute + { + public enum Kind + { + Read, + Write, + Admin + } + + public PermissionAttribute(string type, Kind permission) + { + + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 2469afbc..bbbe177b 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -146,7 +146,7 @@ namespace Kyoo } /// - /// Set every fields of first to those of second. Ignore fields marked with the attribute + /// Set every fields of first to those of second. Ignore fields marked with the attribute /// At the end, the OnMerge method of first will be called if first is a /// /// The object to assign @@ -158,7 +158,7 @@ namespace Kyoo Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null); + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); foreach (PropertyInfo property in properties) { @@ -191,7 +191,7 @@ namespace Kyoo Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null); + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) properties = properties.Where(where); @@ -232,7 +232,7 @@ namespace Kyoo Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null); + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); foreach (PropertyInfo property in properties) { diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 997b7bd4..b58710f3 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Kyoo.Controllers; @@ -463,5 +464,15 @@ namespace Kyoo entry.State = EntityState.Detached; } } + + + /// + /// Perform a case insensitive like operation. + /// + /// An accessor to get the item that will be checked. + /// The second operator of the like format. + /// The type of the item to query + /// An expression representing the like query. It can directly be passed to a where call. + public abstract Expression> Like(Expression> query, string format); } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 826b88e5..ea220cfc 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -1,4 +1,6 @@ using System; +using System.Linq.Expressions; +using System.Reflection; using Kyoo.Models; using Microsoft.EntityFrameworkCore; using Npgsql; @@ -72,5 +74,14 @@ namespace Kyoo.Postgresql { return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; } + + /// + public override Expression> Like(Expression> query, string format) + { + MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.ILike); + MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format)); + + return Expression.Lambda>(call, query.Parameters); + } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index f3eecff0..f56dcac4 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -25,7 +25,7 @@ namespace Kyoo.Postgresql /// public ICollection Provides => new[] { - typeof(PostgresContext) + typeof(DatabaseContext) }; /// diff --git a/Kyoo/Controllers/AuthManager.cs b/Kyoo/Controllers/AuthManager.cs deleted file mode 100644 index 3c2c382d..00000000 --- a/Kyoo/Controllers/AuthManager.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using IdentityServer4.Extensions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Org.BouncyCastle.Asn1.X509; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Generators; -using Org.BouncyCastle.Crypto.Operators; -using Org.BouncyCastle.Math; -using Org.BouncyCastle.Pkcs; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; -using Org.BouncyCastle.X509; -using X509Certificate = Org.BouncyCastle.X509.X509Certificate; - -namespace Kyoo.Controllers -{ - public static class AuthExtension - { - private const string CertificateFile = "certificate.pfx"; - private const string OldCertificateFile = "oldCertificate.pfx"; - - public static IIdentityServerBuilder AddSigninKeys(this IIdentityServerBuilder builder, IConfiguration configuration) - { - X509Certificate2 certificate = GetSiginCredential(configuration); - builder.AddSigningCredential(certificate); - - if (certificate.NotAfter.AddDays(7) <= DateTime.UtcNow) - { - Console.WriteLine("Signin certificate will expire soon, renewing it."); - if (File.Exists(OldCertificateFile)) - File.Delete(OldCertificateFile); - File.Move(CertificateFile, OldCertificateFile); - builder.AddValidationKey(GenerateCertificate(CertificateFile, configuration.GetValue("certificatePassword"))); - } - else if (File.Exists(OldCertificateFile)) - builder.AddValidationKey(GetExistingCredential(OldCertificateFile, configuration.GetValue("certificatePassword"))); - return builder; - } - - private static X509Certificate2 GetSiginCredential(IConfiguration configuration) - { - if (File.Exists(CertificateFile)) - return GetExistingCredential(CertificateFile, configuration.GetValue("certificatePassword")); - return GenerateCertificate(CertificateFile, configuration.GetValue("certificatePassword")); - } - - private static X509Certificate2 GetExistingCredential(string file, string password) - { - return new X509Certificate2(file, password, - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable - ); - } - - private static X509Certificate2 GenerateCertificate(string file, string password) - { - SecureRandom random = new SecureRandom(); - - X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator(); - certificateGenerator.SetSerialNumber(BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random)); - certificateGenerator.SetIssuerDN(new X509Name($"C=NL, O=SDG, CN=Kyoo")); - certificateGenerator.SetSubjectDN(new X509Name($"C=NL, O=SDG, CN=Kyoo")); - certificateGenerator.SetNotBefore(DateTime.UtcNow.Date); - certificateGenerator.SetNotAfter(DateTime.UtcNow.Date.AddMonths(3)); - - KeyGenerationParameters keyGenerationParameters = new KeyGenerationParameters(random, 2048); - RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator(); - keyPairGenerator.Init(keyGenerationParameters); - - AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair(); - certificateGenerator.SetPublicKey(subjectKeyPair.Public); - - AsymmetricCipherKeyPair issuerKeyPair = subjectKeyPair; - const string signatureAlgorithm = "MD5WithRSA"; - Asn1SignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private); - X509Certificate bouncyCert = certificateGenerator.Generate(signatureFactory); - - X509Certificate2 certificate; - - Pkcs12Store store = new Pkcs12StoreBuilder().Build(); - store.SetKeyEntry("Kyoo_key", new AsymmetricKeyEntry(subjectKeyPair.Private), new [] {new X509CertificateEntry(bouncyCert)}); - - using MemoryStream pfxStream = new MemoryStream(); - store.Save(pfxStream, password.ToCharArray(), random); - certificate = new X509Certificate2(pfxStream.ToArray(), password, X509KeyStorageFlags.Exportable); - using FileStream fileStream = File.OpenWrite(file); - pfxStream.WriteTo(fileStream); - return certificate; - } - } - - public class AuthorizationValidatorHandler : AuthorizationHandler - { - private readonly IConfiguration _configuration; - - public AuthorizationValidatorHandler(IConfiguration configuration) - { - _configuration = configuration; - } - - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizationValidator requirement) - { - if (!context.User.IsAuthenticated()) - { - string defaultPerms = _configuration.GetValue("defaultPermissions"); - if (defaultPerms.Split(',').Contains(requirement.Permission.ToLower())) - context.Succeed(requirement); - } - else - { - Claim perms = context.User.Claims.FirstOrDefault(x => x.Type == "permissions"); - if (perms != null && perms.Value.Split(",").Contains(requirement.Permission.ToLower())) - context.Succeed(requirement); - } - - return Task.CompletedTask; - } - } - - public class AuthorizationValidator : IAuthorizationRequirement - { - public string Permission; - - public AuthorizationValidator(string permission) - { - Permission = permission; - } - } -} \ No newline at end of file diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index d2df582e..72d5a9ad 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; using Kyoo.Models.Exceptions; +using Kyoo.UnityExtensions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Unity; @@ -111,7 +113,10 @@ namespace Kyoo.Controllers _logger.LogCritical(error, "A plugin's dependency could not be met"); } else + { plugin.Configure(_container, available); + _container.AddServices(plugin.Configure(new ServiceCollection(), available)); + } } if (!_plugins.Any()) @@ -146,12 +151,16 @@ namespace Kyoo.Controllers .ToList(); if (!needed.Any()) return true; - _logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met", - conditional.Type.Name, - needed.Select(x => x.Name)); + if (log && available.All(x => x != conditional.Type)) + { + _logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met", + conditional.Type.Name, + needed.Select(x => x.Name)); + } return false; } + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (ConditionalProvide conditional in conditionals) { if (IsAvailable(conditional, true)) diff --git a/Kyoo/Controllers/Repositories/CollectionRepository.cs b/Kyoo/Controllers/Repositories/CollectionRepository.cs index bbef77af..e0bc7843 100644 --- a/Kyoo/Controllers/Repositories/CollectionRepository.cs +++ b/Kyoo/Controllers/Repositories/CollectionRepository.cs @@ -35,7 +35,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Collections - .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 75583392..91e39044 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -156,7 +156,8 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { List episodes = await _database.Episodes - .Where(x => EF.Functions.ILike(x.Title, $"%{query}%") && x.EpisodeNumber != -1) + .Where(x => x.EpisodeNumber != -1) + .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/GenreRepository.cs b/Kyoo/Controllers/Repositories/GenreRepository.cs index 0ce1a155..fe9444f5 100644 --- a/Kyoo/Controllers/Repositories/GenreRepository.cs +++ b/Kyoo/Controllers/Repositories/GenreRepository.cs @@ -36,7 +36,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Genres - .Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 05a9f6ab..ab872a50 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -101,7 +101,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await ItemsQuery - .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index b4cab3b9..affd899c 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -43,7 +43,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Libraries - .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index a3b20e38..2c9c6156 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { @@ -52,7 +51,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.People - .Where(people => EF.Functions.ILike(people.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index 31c283d3..135e8148 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -36,7 +36,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Providers - .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index e86a812b..a6ef7fcd 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -121,7 +121,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { List seasons = await _database.Seasons - .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 07d2cafd..41a1bb0a 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -79,9 +79,7 @@ namespace Kyoo.Controllers { query = $"%{query}%"; return await _database.Shows - .Where(x => EF.Functions.ILike(x.Title, query) - || EF.Functions.ILike(x.Slug, query) - /*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE. + .Where(_database.Like(x => x.Title + " " + x.Slug, query)) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Controllers/Repositories/StudioRepository.cs b/Kyoo/Controllers/Repositories/StudioRepository.cs index 6c813f65..516b7c08 100644 --- a/Kyoo/Controllers/Repositories/StudioRepository.cs +++ b/Kyoo/Controllers/Repositories/StudioRepository.cs @@ -36,7 +36,7 @@ namespace Kyoo.Controllers public override async Task> Search(string query) { return await _database.Studios - .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .Where(_database.Like(x => x.Name, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) .ToListAsync(); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 40a16662..5eaff52e 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -35,18 +35,7 @@ - - - - - - - - - - - diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs deleted file mode 100644 index 5d406433..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs +++ /dev/null @@ -1,985 +0,0 @@ -// -using System; -using IdentityServer4.EntityFramework.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration -{ - [DbContext(typeof(ConfigurationDbContext))] - [Migration("20210306161631_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AllowedAccessTokenSigningAlgorithms") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("LastAccessed") - .HasColumnType("timestamp without time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("ApiResources"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Scope") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceSecrets"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Emphasize") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Required") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("ApiScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ScopeId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ScopeId"); - - b.ToTable("ApiScopeClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("ScopeId") - .HasColumnType("integer"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ScopeId"); - - b.ToTable("ApiScopeProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AbsoluteRefreshTokenLifetime") - .HasColumnType("integer"); - - b.Property("AccessTokenLifetime") - .HasColumnType("integer"); - - b.Property("AccessTokenType") - .HasColumnType("integer"); - - b.Property("AllowAccessTokensViaBrowser") - .HasColumnType("boolean"); - - b.Property("AllowOfflineAccess") - .HasColumnType("boolean"); - - b.Property("AllowPlainTextPkce") - .HasColumnType("boolean"); - - b.Property("AllowRememberConsent") - .HasColumnType("boolean"); - - b.Property("AllowedIdentityTokenSigningAlgorithms") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AlwaysIncludeUserClaimsInIdToken") - .HasColumnType("boolean"); - - b.Property("AlwaysSendClientClaims") - .HasColumnType("boolean"); - - b.Property("AuthorizationCodeLifetime") - .HasColumnType("integer"); - - b.Property("BackChannelLogoutSessionRequired") - .HasColumnType("boolean"); - - b.Property("BackChannelLogoutUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ClientClaimsPrefix") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ConsentLifetime") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DeviceCodeLifetime") - .HasColumnType("integer"); - - b.Property("EnableLocalLogin") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("FrontChannelLogoutSessionRequired") - .HasColumnType("boolean"); - - b.Property("FrontChannelLogoutUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("IdentityTokenLifetime") - .HasColumnType("integer"); - - b.Property("IncludeJwtId") - .HasColumnType("boolean"); - - b.Property("LastAccessed") - .HasColumnType("timestamp without time zone"); - - b.Property("LogoUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("PairWiseSubjectSalt") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProtocolType") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("RefreshTokenExpiration") - .HasColumnType("integer"); - - b.Property("RefreshTokenUsage") - .HasColumnType("integer"); - - b.Property("RequireClientSecret") - .HasColumnType("boolean"); - - b.Property("RequireConsent") - .HasColumnType("boolean"); - - b.Property("RequirePkce") - .HasColumnType("boolean"); - - b.Property("RequireRequestObject") - .HasColumnType("boolean"); - - b.Property("SlidingRefreshTokenLifetime") - .HasColumnType("integer"); - - b.Property("UpdateAccessTokenClaimsOnRefresh") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.Property("UserCodeType") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UserSsoLifetime") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("Clients"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Origin") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientCorsOrigins"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("GrantType") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientGrantTypes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Provider") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientIdPRestrictions"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("PostLogoutRedirectUri") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientPostLogoutRedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("RedirectUri") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientRedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Scope") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientSecrets"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Emphasize") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("Required") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("IdentityResources"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityResourceClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityResourceProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("UserClaims") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Properties") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Scopes") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Secrets") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") - .WithMany("UserClaims") - .HasForeignKey("ScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scope"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") - .WithMany("Properties") - .HasForeignKey("ScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scope"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("Claims") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedCorsOrigins") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedGrantTypes") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("IdentityProviderRestrictions") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("PostLogoutRedirectUris") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("Properties") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("RedirectUris") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedScopes") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("ClientSecrets") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") - .WithMany("UserClaims") - .HasForeignKey("IdentityResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("IdentityResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") - .WithMany("Properties") - .HasForeignKey("IdentityResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("IdentityResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => - { - b.Navigation("Properties"); - - b.Navigation("Scopes"); - - b.Navigation("Secrets"); - - b.Navigation("UserClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => - { - b.Navigation("Properties"); - - b.Navigation("UserClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => - { - b.Navigation("AllowedCorsOrigins"); - - b.Navigation("AllowedGrantTypes"); - - b.Navigation("AllowedScopes"); - - b.Navigation("Claims"); - - b.Navigation("ClientSecrets"); - - b.Navigation("IdentityProviderRestrictions"); - - b.Navigation("PostLogoutRedirectUris"); - - b.Navigation("Properties"); - - b.Navigation("RedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => - { - b.Navigation("Properties"); - - b.Navigation("UserClaims"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs deleted file mode 100644 index fec7b0f0..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs +++ /dev/null @@ -1,658 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ApiResources", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(type: "boolean", nullable: false), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - AllowedAccessTokenSigningAlgorithms = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false), - Created = table.Column(type: "timestamp without time zone", nullable: false), - Updated = table.Column(type: "timestamp without time zone", nullable: true), - LastAccessed = table.Column(type: "timestamp without time zone", nullable: true), - NonEditable = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiResources", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ApiScopes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(type: "boolean", nullable: false), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - Required = table.Column(type: "boolean", nullable: false), - Emphasize = table.Column(type: "boolean", nullable: false), - ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiScopes", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Clients", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(type: "boolean", nullable: false), - ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - ProtocolType = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - RequireClientSecret = table.Column(type: "boolean", nullable: false), - ClientName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - ClientUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - LogoUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - RequireConsent = table.Column(type: "boolean", nullable: false), - AllowRememberConsent = table.Column(type: "boolean", nullable: false), - AlwaysIncludeUserClaimsInIdToken = table.Column(type: "boolean", nullable: false), - RequirePkce = table.Column(type: "boolean", nullable: false), - AllowPlainTextPkce = table.Column(type: "boolean", nullable: false), - RequireRequestObject = table.Column(type: "boolean", nullable: false), - AllowAccessTokensViaBrowser = table.Column(type: "boolean", nullable: false), - FrontChannelLogoutUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - FrontChannelLogoutSessionRequired = table.Column(type: "boolean", nullable: false), - BackChannelLogoutUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - BackChannelLogoutSessionRequired = table.Column(type: "boolean", nullable: false), - AllowOfflineAccess = table.Column(type: "boolean", nullable: false), - IdentityTokenLifetime = table.Column(type: "integer", nullable: false), - AllowedIdentityTokenSigningAlgorithms = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - AccessTokenLifetime = table.Column(type: "integer", nullable: false), - AuthorizationCodeLifetime = table.Column(type: "integer", nullable: false), - ConsentLifetime = table.Column(type: "integer", nullable: true), - AbsoluteRefreshTokenLifetime = table.Column(type: "integer", nullable: false), - SlidingRefreshTokenLifetime = table.Column(type: "integer", nullable: false), - RefreshTokenUsage = table.Column(type: "integer", nullable: false), - UpdateAccessTokenClaimsOnRefresh = table.Column(type: "boolean", nullable: false), - RefreshTokenExpiration = table.Column(type: "integer", nullable: false), - AccessTokenType = table.Column(type: "integer", nullable: false), - EnableLocalLogin = table.Column(type: "boolean", nullable: false), - IncludeJwtId = table.Column(type: "boolean", nullable: false), - AlwaysSendClientClaims = table.Column(type: "boolean", nullable: false), - ClientClaimsPrefix = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - PairWiseSubjectSalt = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - Created = table.Column(type: "timestamp without time zone", nullable: false), - Updated = table.Column(type: "timestamp without time zone", nullable: true), - LastAccessed = table.Column(type: "timestamp without time zone", nullable: true), - UserSsoLifetime = table.Column(type: "integer", nullable: true), - UserCodeType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - DeviceCodeLifetime = table.Column(type: "integer", nullable: false), - NonEditable = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Clients", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "IdentityResources", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(type: "boolean", nullable: false), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - Required = table.Column(type: "boolean", nullable: false), - Emphasize = table.Column(type: "boolean", nullable: false), - ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false), - Created = table.Column(type: "timestamp without time zone", nullable: false), - Updated = table.Column(type: "timestamp without time zone", nullable: true), - NonEditable = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IdentityResources", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ApiResourceClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ApiResourceId = table.Column(type: "integer", nullable: false), - Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiResourceClaims", x => x.Id); - table.ForeignKey( - name: "FK_ApiResourceClaims_ApiResources_ApiResourceId", - column: x => x.ApiResourceId, - principalTable: "ApiResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ApiResourceProperties", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ApiResourceId = table.Column(type: "integer", nullable: false), - Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiResourceProperties", x => x.Id); - table.ForeignKey( - name: "FK_ApiResourceProperties_ApiResources_ApiResourceId", - column: x => x.ApiResourceId, - principalTable: "ApiResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ApiResourceScopes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Scope = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - ApiResourceId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiResourceScopes", x => x.Id); - table.ForeignKey( - name: "FK_ApiResourceScopes_ApiResources_ApiResourceId", - column: x => x.ApiResourceId, - principalTable: "ApiResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ApiResourceSecrets", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ApiResourceId = table.Column(type: "integer", nullable: false), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - Value = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), - Expiration = table.Column(type: "timestamp without time zone", nullable: true), - Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Created = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id); - table.ForeignKey( - name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId", - column: x => x.ApiResourceId, - principalTable: "ApiResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ApiScopeClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ScopeId = table.Column(type: "integer", nullable: false), - Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiScopeClaims", x => x.Id); - table.ForeignKey( - name: "FK_ApiScopeClaims_ApiScopes_ScopeId", - column: x => x.ScopeId, - principalTable: "ApiScopes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ApiScopeProperties", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ScopeId = table.Column(type: "integer", nullable: false), - Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiScopeProperties", x => x.Id); - table.ForeignKey( - name: "FK_ApiScopeProperties_ApiScopes_ScopeId", - column: x => x.ScopeId, - principalTable: "ApiScopes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Value = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientClaims", x => x.Id); - table.ForeignKey( - name: "FK_ClientClaims_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientCorsOrigins", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Origin = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id); - table.ForeignKey( - name: "FK_ClientCorsOrigins_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientGrantTypes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - GrantType = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientGrantTypes", x => x.Id); - table.ForeignKey( - name: "FK_ClientGrantTypes_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientIdPRestrictions", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Provider = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id); - table.ForeignKey( - name: "FK_ClientIdPRestrictions_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientPostLogoutRedirectUris", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - PostLogoutRedirectUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id); - table.ForeignKey( - name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientProperties", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ClientId = table.Column(type: "integer", nullable: false), - Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientProperties", x => x.Id); - table.ForeignKey( - name: "FK_ClientProperties_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientRedirectUris", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RedirectUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientRedirectUris", x => x.Id); - table.ForeignKey( - name: "FK_ClientRedirectUris_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientScopes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Scope = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - ClientId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientScopes", x => x.Id); - table.ForeignKey( - name: "FK_ClientScopes_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ClientSecrets", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ClientId = table.Column(type: "integer", nullable: false), - Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - Value = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), - Expiration = table.Column(type: "timestamp without time zone", nullable: true), - Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Created = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ClientSecrets", x => x.Id); - table.ForeignKey( - name: "FK_ClientSecrets_Clients_ClientId", - column: x => x.ClientId, - principalTable: "Clients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "IdentityResourceClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - IdentityResourceId = table.Column(type: "integer", nullable: false), - Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id); - table.ForeignKey( - name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId", - column: x => x.IdentityResourceId, - principalTable: "IdentityResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "IdentityResourceProperties", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - IdentityResourceId = table.Column(type: "integer", nullable: false), - Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), - Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id); - table.ForeignKey( - name: "FK_IdentityResourceProperties_IdentityResources_IdentityResour~", - column: x => x.IdentityResourceId, - principalTable: "IdentityResources", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ApiResourceClaims_ApiResourceId", - table: "ApiResourceClaims", - column: "ApiResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiResourceProperties_ApiResourceId", - table: "ApiResourceProperties", - column: "ApiResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiResources_Name", - table: "ApiResources", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ApiResourceScopes_ApiResourceId", - table: "ApiResourceScopes", - column: "ApiResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiResourceSecrets_ApiResourceId", - table: "ApiResourceSecrets", - column: "ApiResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiScopeClaims_ScopeId", - table: "ApiScopeClaims", - column: "ScopeId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiScopeProperties_ScopeId", - table: "ApiScopeProperties", - column: "ScopeId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiScopes_Name", - table: "ApiScopes", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ClientClaims_ClientId", - table: "ClientClaims", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientCorsOrigins_ClientId", - table: "ClientCorsOrigins", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientGrantTypes_ClientId", - table: "ClientGrantTypes", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientIdPRestrictions_ClientId", - table: "ClientIdPRestrictions", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientPostLogoutRedirectUris_ClientId", - table: "ClientPostLogoutRedirectUris", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientProperties_ClientId", - table: "ClientProperties", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientRedirectUris_ClientId", - table: "ClientRedirectUris", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_Clients_ClientId", - table: "Clients", - column: "ClientId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ClientScopes_ClientId", - table: "ClientScopes", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_ClientSecrets_ClientId", - table: "ClientSecrets", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_IdentityResourceClaims_IdentityResourceId", - table: "IdentityResourceClaims", - column: "IdentityResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_IdentityResourceProperties_IdentityResourceId", - table: "IdentityResourceProperties", - column: "IdentityResourceId"); - - migrationBuilder.CreateIndex( - name: "IX_IdentityResources_Name", - table: "IdentityResources", - column: "Name", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ApiResourceClaims"); - - migrationBuilder.DropTable( - name: "ApiResourceProperties"); - - migrationBuilder.DropTable( - name: "ApiResourceScopes"); - - migrationBuilder.DropTable( - name: "ApiResourceSecrets"); - - migrationBuilder.DropTable( - name: "ApiScopeClaims"); - - migrationBuilder.DropTable( - name: "ApiScopeProperties"); - - migrationBuilder.DropTable( - name: "ClientClaims"); - - migrationBuilder.DropTable( - name: "ClientCorsOrigins"); - - migrationBuilder.DropTable( - name: "ClientGrantTypes"); - - migrationBuilder.DropTable( - name: "ClientIdPRestrictions"); - - migrationBuilder.DropTable( - name: "ClientPostLogoutRedirectUris"); - - migrationBuilder.DropTable( - name: "ClientProperties"); - - migrationBuilder.DropTable( - name: "ClientRedirectUris"); - - migrationBuilder.DropTable( - name: "ClientScopes"); - - migrationBuilder.DropTable( - name: "ClientSecrets"); - - migrationBuilder.DropTable( - name: "IdentityResourceClaims"); - - migrationBuilder.DropTable( - name: "IdentityResourceProperties"); - - migrationBuilder.DropTable( - name: "ApiResources"); - - migrationBuilder.DropTable( - name: "ApiScopes"); - - migrationBuilder.DropTable( - name: "Clients"); - - migrationBuilder.DropTable( - name: "IdentityResources"); - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs deleted file mode 100644 index c11ac33a..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs +++ /dev/null @@ -1,983 +0,0 @@ -// -using System; -using IdentityServer4.EntityFramework.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration -{ - [DbContext(typeof(ConfigurationDbContext))] - partial class ConfigurationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AllowedAccessTokenSigningAlgorithms") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("LastAccessed") - .HasColumnType("timestamp without time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("ApiResources"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Scope") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.HasKey("Id"); - - b.HasIndex("ApiResourceId"); - - b.ToTable("ApiResourceSecrets"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Emphasize") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Required") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("ApiScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ScopeId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ScopeId"); - - b.ToTable("ApiScopeClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("ScopeId") - .HasColumnType("integer"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ScopeId"); - - b.ToTable("ApiScopeProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AbsoluteRefreshTokenLifetime") - .HasColumnType("integer"); - - b.Property("AccessTokenLifetime") - .HasColumnType("integer"); - - b.Property("AccessTokenType") - .HasColumnType("integer"); - - b.Property("AllowAccessTokensViaBrowser") - .HasColumnType("boolean"); - - b.Property("AllowOfflineAccess") - .HasColumnType("boolean"); - - b.Property("AllowPlainTextPkce") - .HasColumnType("boolean"); - - b.Property("AllowRememberConsent") - .HasColumnType("boolean"); - - b.Property("AllowedIdentityTokenSigningAlgorithms") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AlwaysIncludeUserClaimsInIdToken") - .HasColumnType("boolean"); - - b.Property("AlwaysSendClientClaims") - .HasColumnType("boolean"); - - b.Property("AuthorizationCodeLifetime") - .HasColumnType("integer"); - - b.Property("BackChannelLogoutSessionRequired") - .HasColumnType("boolean"); - - b.Property("BackChannelLogoutUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ClientClaimsPrefix") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ConsentLifetime") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DeviceCodeLifetime") - .HasColumnType("integer"); - - b.Property("EnableLocalLogin") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("FrontChannelLogoutSessionRequired") - .HasColumnType("boolean"); - - b.Property("FrontChannelLogoutUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("IdentityTokenLifetime") - .HasColumnType("integer"); - - b.Property("IncludeJwtId") - .HasColumnType("boolean"); - - b.Property("LastAccessed") - .HasColumnType("timestamp without time zone"); - - b.Property("LogoUri") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("PairWiseSubjectSalt") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProtocolType") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("RefreshTokenExpiration") - .HasColumnType("integer"); - - b.Property("RefreshTokenUsage") - .HasColumnType("integer"); - - b.Property("RequireClientSecret") - .HasColumnType("boolean"); - - b.Property("RequireConsent") - .HasColumnType("boolean"); - - b.Property("RequirePkce") - .HasColumnType("boolean"); - - b.Property("RequireRequestObject") - .HasColumnType("boolean"); - - b.Property("SlidingRefreshTokenLifetime") - .HasColumnType("integer"); - - b.Property("UpdateAccessTokenClaimsOnRefresh") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.Property("UserCodeType") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UserSsoLifetime") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("Clients"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Origin") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientCorsOrigins"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("GrantType") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientGrantTypes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Provider") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientIdPRestrictions"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("PostLogoutRedirectUri") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientPostLogoutRedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("RedirectUri") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientRedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Scope") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientScopes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClientId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId"); - - b.ToTable("ClientSecrets"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("DisplayName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Emphasize") - .HasColumnType("boolean"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("NonEditable") - .HasColumnType("boolean"); - - b.Property("Required") - .HasColumnType("boolean"); - - b.Property("ShowInDiscoveryDocument") - .HasColumnType("boolean"); - - b.Property("Updated") - .HasColumnType("timestamp without time zone"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("IdentityResources"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityResourceClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityResourceProperties"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("UserClaims") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Properties") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Scopes") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") - .WithMany("Secrets") - .HasForeignKey("ApiResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ApiResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") - .WithMany("UserClaims") - .HasForeignKey("ScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scope"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") - .WithMany("Properties") - .HasForeignKey("ScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scope"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("Claims") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedCorsOrigins") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedGrantTypes") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("IdentityProviderRestrictions") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("PostLogoutRedirectUris") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("Properties") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("RedirectUris") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("AllowedScopes") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") - .WithMany("ClientSecrets") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") - .WithMany("UserClaims") - .HasForeignKey("IdentityResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("IdentityResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") - .WithMany("Properties") - .HasForeignKey("IdentityResourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("IdentityResource"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => - { - b.Navigation("Properties"); - - b.Navigation("Scopes"); - - b.Navigation("Secrets"); - - b.Navigation("UserClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => - { - b.Navigation("Properties"); - - b.Navigation("UserClaims"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => - { - b.Navigation("AllowedCorsOrigins"); - - b.Navigation("AllowedGrantTypes"); - - b.Navigation("AllowedScopes"); - - b.Navigation("Claims"); - - b.Navigation("ClientSecrets"); - - b.Navigation("IdentityProviderRestrictions"); - - b.Navigation("PostLogoutRedirectUris"); - - b.Navigation("Properties"); - - b.Navigation("RedirectUris"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => - { - b.Navigation("Properties"); - - b.Navigation("UserClaims"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs deleted file mode 100644 index 10626a22..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs +++ /dev/null @@ -1,384 +0,0 @@ -// -using System; -using Kyoo; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase -{ - [DbContext(typeof(IdentityDatabase))] - [Migration("20210216205030_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => - { - b.Property("UserCode") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Data") - .IsRequired() - .HasMaxLength(50000) - .HasColumnType("character varying(50000)"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("DeviceCode") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Expiration") - .IsRequired() - .HasColumnType("timestamp without time zone"); - - b.Property("SessionId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SubjectId") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("UserCode"); - - b.HasIndex("DeviceCode") - .IsUnique(); - - b.HasIndex("Expiration"); - - b.ToTable("DeviceCodes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => - { - b.Property("Key") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ConsumedTime") - .HasColumnType("timestamp without time zone"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Data") - .IsRequired() - .HasMaxLength(50000) - .HasColumnType("character varying(50000)"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("SessionId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SubjectId") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Key"); - - b.HasIndex("Expiration"); - - b.HasIndex("SubjectId", "ClientId", "Type"); - - b.HasIndex("SubjectId", "SessionId", "Type"); - - b.ToTable("PersistedGrants"); - }); - - modelBuilder.Entity("Kyoo.Models.User", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OTAC") - .HasColumnType("text"); - - b.Property("OTACExpires") - .HasColumnType("timestamp without time zone"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("UserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRoleClaim"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("UserClaim"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProviderKey") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("UserLogin"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRole"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Name") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("UserToken"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs deleted file mode 100644 index e0cffa4f..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "DeviceCodes", - columns: table => new - { - UserCode = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - DeviceCode = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - SubjectId = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - CreationTime = table.Column(type: "timestamp without time zone", nullable: false), - Expiration = table.Column(type: "timestamp without time zone", nullable: false), - Data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); - }); - - migrationBuilder.CreateTable( - name: "PersistedGrants", - columns: table => new - { - Key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - SubjectId = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - CreationTime = table.Column(type: "timestamp without time zone", nullable: false), - Expiration = table.Column(type: "timestamp without time zone", nullable: true), - ConsumedTime = table.Column(type: "timestamp without time zone", nullable: true), - Data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PersistedGrants", x => x.Key); - }); - - migrationBuilder.CreateTable( - name: "User", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - OTAC = table.Column(type: "text", nullable: true), - OTACExpires = table.Column(type: "timestamp without time zone", nullable: true), - UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EmailConfirmed = table.Column(type: "boolean", nullable: false), - PasswordHash = table.Column(type: "text", nullable: true), - SecurityStamp = table.Column(type: "text", nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true), - PhoneNumber = table.Column(type: "text", nullable: true), - PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), - TwoFactorEnabled = table.Column(type: "boolean", nullable: false), - LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), - LockoutEnabled = table.Column(type: "boolean", nullable: false), - AccessFailedCount = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_User", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserRoles", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_UserRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserClaim", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "text", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_UserClaim", x => x.Id); - table.ForeignKey( - name: "FK_UserClaim_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserLogin", - columns: table => new - { - LoginProvider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ProviderKey = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ProviderDisplayName = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserLogin", x => new { x.LoginProvider, x.ProviderKey }); - table.ForeignKey( - name: "FK_UserLogin_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserToken", - columns: table => new - { - UserId = table.Column(type: "text", nullable: false), - LoginProvider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_UserToken", x => new { x.UserId, x.LoginProvider, x.Name }); - table.ForeignKey( - name: "FK_UserToken_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserRole", - columns: table => new - { - UserId = table.Column(type: "text", nullable: false), - RoleId = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_UserRole_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_UserRole_UserRoles_RoleId", - column: x => x.RoleId, - principalTable: "UserRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserRoleClaim", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RoleId = table.Column(type: "text", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_UserRoleClaim", x => x.Id); - table.ForeignKey( - name: "FK_UserRoleClaim_UserRoles_RoleId", - column: x => x.RoleId, - principalTable: "UserRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_DeviceCodes_DeviceCode", - table: "DeviceCodes", - column: "DeviceCode", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_DeviceCodes_Expiration", - table: "DeviceCodes", - column: "Expiration"); - - migrationBuilder.CreateIndex( - name: "IX_PersistedGrants_Expiration", - table: "PersistedGrants", - column: "Expiration"); - - migrationBuilder.CreateIndex( - name: "IX_PersistedGrants_SubjectId_ClientId_Type", - table: "PersistedGrants", - columns: new[] { "SubjectId", "ClientId", "Type" }); - - migrationBuilder.CreateIndex( - name: "IX_PersistedGrants_SubjectId_SessionId_Type", - table: "PersistedGrants", - columns: new[] { "SubjectId", "SessionId", "Type" }); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "User", - column: "NormalizedEmail"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "User", - column: "NormalizedUserName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserClaim_UserId", - table: "UserClaim", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_UserLogin_UserId", - table: "UserLogin", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_UserRole_RoleId", - table: "UserRole", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "IX_UserRoleClaim_RoleId", - table: "UserRoleClaim", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "UserRoles", - column: "NormalizedName", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DeviceCodes"); - - migrationBuilder.DropTable( - name: "PersistedGrants"); - - migrationBuilder.DropTable( - name: "UserClaim"); - - migrationBuilder.DropTable( - name: "UserLogin"); - - migrationBuilder.DropTable( - name: "UserRole"); - - migrationBuilder.DropTable( - name: "UserRoleClaim"); - - migrationBuilder.DropTable( - name: "UserToken"); - - migrationBuilder.DropTable( - name: "UserRoles"); - - migrationBuilder.DropTable( - name: "User"); - } - } -} diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs deleted file mode 100644 index fa2da994..00000000 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs +++ /dev/null @@ -1,382 +0,0 @@ -// -using System; -using Kyoo; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase -{ - [DbContext(typeof(IdentityDatabase))] - partial class IdentityDatabaseModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.3") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => - { - b.Property("UserCode") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Data") - .IsRequired() - .HasMaxLength(50000) - .HasColumnType("character varying(50000)"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("DeviceCode") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Expiration") - .IsRequired() - .HasColumnType("timestamp without time zone"); - - b.Property("SessionId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SubjectId") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("UserCode"); - - b.HasIndex("DeviceCode") - .IsUnique(); - - b.HasIndex("Expiration"); - - b.ToTable("DeviceCodes"); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => - { - b.Property("Key") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ConsumedTime") - .HasColumnType("timestamp without time zone"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Data") - .IsRequired() - .HasMaxLength(50000) - .HasColumnType("character varying(50000)"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("SessionId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SubjectId") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Key"); - - b.HasIndex("Expiration"); - - b.HasIndex("SubjectId", "ClientId", "Type"); - - b.HasIndex("SubjectId", "SessionId", "Type"); - - b.ToTable("PersistedGrants"); - }); - - modelBuilder.Entity("Kyoo.Models.User", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OTAC") - .HasColumnType("text"); - - b.Property("OTACExpires") - .HasColumnType("timestamp without time zone"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("UserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRoleClaim"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("UserClaim"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProviderKey") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("UserLogin"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRole"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Name") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("UserToken"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Kyoo.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Kyoo/Models/IdentityDatabase.cs b/Kyoo/Models/IdentityDatabase.cs deleted file mode 100644 index f70f07b3..00000000 --- a/Kyoo/Models/IdentityDatabase.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; -using IdentityServer4.EntityFramework.Entities; -using IdentityServer4.EntityFramework.Extensions; -using IdentityServer4.EntityFramework.Interfaces; -using IdentityServer4.EntityFramework.Options; -using Kyoo.Models; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Kyoo -{ - // The configuration's database is named ConfigurationDbContext. - public class IdentityDatabase : IdentityDbContext, IPersistedGrantDbContext - { - private readonly IOptions _operationalStoreOptions; - - public IdentityDatabase(DbContextOptions options, IOptions operationalStoreOptions) - : base(options) - { - _operationalStoreOptions = operationalStoreOptions; - } - - public DbSet Accounts { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value); - - modelBuilder.Entity().ToTable("User"); - modelBuilder.Entity>().ToTable("UserRole"); - modelBuilder.Entity>().ToTable("UserLogin"); - modelBuilder.Entity>().ToTable("UserClaim"); - modelBuilder.Entity().ToTable("UserRoles"); - modelBuilder.Entity>().ToTable("UserRoleClaim"); - modelBuilder.Entity>().ToTable("UserToken"); - } - - public Task SaveChangesAsync() => base.SaveChangesAsync(); - - public DbSet PersistedGrants { get; set; } - public DbSet DeviceFlowCodes { get; set; } - - } -} \ No newline at end of file diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index d747c960..925d25e1 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,25 +1,13 @@ using System; using System.IO; -using System.Reflection; -using IdentityServer4.Extensions; -using IdentityServer4.Services; -using Kyoo.Api; using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.StaticFiles; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Unity; using Unity.Lifetime; @@ -31,13 +19,11 @@ namespace Kyoo public class Startup { private readonly IConfiguration _configuration; - private readonly ILoggerFactory _loggerFactory; - public Startup(IConfiguration configuration, ILoggerFactory loggerFactory) + public Startup(IConfiguration configuration) { _configuration = configuration; - _loggerFactory = loggerFactory; } public void ConfigureServices(IServiceCollection services) @@ -62,86 +48,17 @@ namespace Kyoo x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); }); services.AddHttpClient(); - - services.AddDbContext(options => - { - options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); - }); - - string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; - - services.AddIdentityCore(o => - { - o.Stores.MaxLengthForKeys = 128; - }) - .AddSignInManager() - .AddDefaultTokenProviders() - .AddEntityFrameworkStores(); - - services.AddIdentityServer(options => - { - options.IssuerUri = publicUrl; - options.UserInteraction.LoginUrl = publicUrl + "login"; - options.UserInteraction.ErrorUrl = publicUrl + "error"; - options.UserInteraction.LogoutUrl = publicUrl + "logout"; - }) - .AddAspNetIdentity() - .AddConfigurationStore(options => - { - options.ConfigureDbContext = builder => - builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), - sql => sql.MigrationsAssembly(assemblyName)); - }) - .AddOperationalStore(options => - { - options.ConfigureDbContext = builder => - builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), - sql => sql.MigrationsAssembly(assemblyName)); - options.EnableTokenCleanup = true; - }) - .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) - .AddInMemoryApiScopes(IdentityContext.GetScopes()) - .AddInMemoryApiResources(IdentityContext.GetApis()) - .AddProfileService() - .AddSigninKeys(_configuration); - services.AddAuthentication(o => - { - o.DefaultScheme = IdentityConstants.ApplicationScheme; - o.DefaultSignInScheme = IdentityConstants.ExternalScheme; - }) - .AddIdentityCookies(_ => { }); - services.AddAuthentication() - .AddJwtBearer(options => - { - options.Authority = publicUrl; - options.Audience = "Kyoo"; - options.RequireHttpsMetadata = false; - }); - - services.AddAuthorization(options => - { - AuthorizationPolicyBuilder scheme = new(IdentityConstants.ApplicationScheme, JwtBearerDefaults.AuthenticationScheme); - options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build(); - - string[] permissions = {"Read", "Write", "Play", "Admin"}; - foreach (string permission in permissions) - { - options.AddPolicy(permission, policy => - { - policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme); - policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); - policy.AddRequirements(new AuthorizationValidator(permission)); - // policy.RequireScope($"kyoo.{permission.ToLower()}"); - }); - } - }); - services.AddSingleton(); - - services.AddSingleton(new DefaultCorsPolicyService(_loggerFactory.CreateLogger()) - { - AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } - }); + // services.AddAuthorization(options => + // { + // string[] permissions = {"Read", "Write", "Play", "Admin"}; + // foreach (string permission in permissions) + // options.AddPolicy(permission, policy => + // { + // policy.AddRequirements(new AuthorizationValidator(permission)); + // }); + // }); + // services.AddAuthentication() services.AddSingleton(); services.AddHostedService(x => x.GetService() as TaskManager); @@ -152,9 +69,7 @@ namespace Kyoo public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) - { app.UseDeveloperExceptionPage(); - } else { app.UseExceptionHandler("/Error"); @@ -172,6 +87,7 @@ namespace Kyoo app.UseSpaStaticFiles(); app.UseRouting(); + // app.UseAuthorization(); app.Use((ctx, next) => { @@ -186,36 +102,26 @@ namespace Kyoo return next(); }); app.UseResponseCompression(); - app.UseCookiePolicy(new CookiePolicyOptions - { - MinimumSameSitePolicy = SameSiteMode.Strict - }); - app.UseAuthentication(); - app.Use((ctx, next) => - { - ctx.SetIdentityServerOrigin(_configuration.GetValue("public_url")); - return next(); - }); - app.UseIdentityServer(); - app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); - }); - - app.UseSpa(spa => - { - spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - - if (env.IsDevelopment()) - spa.UseAngularCliServer("start"); - }); - + // app.UseSpa(spa => + // { + // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); + // + // if (env.IsDevelopment()) + // spa.UseAngularCliServer("start"); + // }); + // container.RegisterType(new SingletonLifetimeManager()); // container.Resolve(); IPluginManager pluginManager = container.Resolve(); pluginManager.ReloadPlugins(); + foreach (IPlugin plugin in pluginManager.GetAllPlugins()) + plugin.ConfigureAspNet(app); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); + }); } } } diff --git a/Kyoo/Views/TaskApi.cs b/Kyoo/Views/TaskApi.cs index 1efb3cad..91aa2a5d 100644 --- a/Kyoo/Views/TaskApi.cs +++ b/Kyoo/Views/TaskApi.cs @@ -2,15 +2,17 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Kyoo.Controllers; +using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; +using static Kyoo.Models.Attributes.PermissionAttribute; namespace Kyoo.Api { [Route("api/task")] [Route("api/tasks")] [ApiController] - [Authorize(Policy="Admin")] + // [Authorize(Policy="Admin")] public class TaskApi : ControllerBase { private readonly ITaskManager _taskManager; @@ -22,6 +24,7 @@ namespace Kyoo.Api [HttpGet] + [Permission("task", Kind.Read)] public ActionResult> GetTasks() { return Ok(_taskManager.GetAllTasks()); diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 1622fe15..bcfc92d3 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -17,21 +17,25 @@ "logging": { "logLevel": { - "default": "Warning", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", + "default": "Trace", "Kyoo": "Trace" } }, + "authentication": { + "certificate": { + "file": "certificate.pfx", + "oldFile": "oldCertificate.pfx", + "password": "passphrase" + } + }, + "parallelTasks": "1", "scheduledTasks": { "scan": "24:00:00" }, - - "certificatePassword": "passphrase", "transmuxTempPath": "cached/kyoo/transmux", "transcodeTempPath": "cached/kyoo/transcode", From a18f393926a0cd5ffaa1299e60d293a572dbb637 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 3 May 2021 23:33:48 +0200 Subject: [PATCH 10/25] Fixing task arguments --- Kyoo.Authentication/AuthenticationModule.cs | 1 + Kyoo.Postgresql/Kyoo.Postgresql.csproj | 1 + Kyoo/Controllers/PluginManager.cs | 1 + Kyoo/Controllers/TaskManager.cs | 69 ++++++++++++++------- Kyoo/Program.cs | 4 +- Kyoo/Startup.cs | 36 +++++++---- Kyoo/settings.json | 2 + 7 files changed, 76 insertions(+), 38 deletions(-) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index b9e575c5..4260512c 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -105,6 +105,7 @@ namespace Kyoo.Authentication .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) + .AddInMemoryClients(IdentityContext.GetClients()) // .AddProfileService() .AddSigninKeys(certificateOptions); diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index b870c769..65f2f5e5 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -7,6 +7,7 @@ false false false + true SDG Zoe Roux diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 72d5a9ad..7c1be980 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -145,6 +145,7 @@ namespace Kyoo.Controllers ICollection needed = conditional.Condition.Needed .Where(y => !available.Contains(y)) .ToList(); + // TODO handle circular dependencies, actually it might stack overflow. needed = needed.Where(x => !conditionals .Where(y => y.Type == x) .Any(y => IsAvailable(y))) diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 89cff352..437b8b14 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -24,7 +24,7 @@ namespace Kyoo.Controllers /// private readonly IUnityContainer _container; /// - /// The configuration instance used to get schedule informations + /// The configuration instance used to get schedule information /// private readonly IConfiguration _configuration; /// @@ -37,7 +37,7 @@ namespace Kyoo.Controllers /// private List<(ITask task, DateTime scheduledDate)> _tasks; /// - /// The queue of tasks that should be runned as soon as possible. + /// The queue of tasks that should be run as soon as possible. /// private readonly Queue<(ITask, Dictionary)> _queuedTasks = new(); /// @@ -108,27 +108,11 @@ namespace Kyoo.Controllers _runningTask = task; try { - ICollection all = task.GetParameters(); - TaskParameters args = new(arguments - .Select(x => (value: x, arg: all - .FirstOrDefault(y => string.Equals(y.Name, x.Key, StringComparison.OrdinalIgnoreCase)))) - .Select(x => - { - if (x.arg == null) - throw new ArgumentException($"Invalid argument name: {x.value.Key}"); - return x.arg.CreateValue(x.value.Value); - })); - - - _logger.LogInformation("Task starting: {Task}", task.Name); - InjectServices(task); - await task.Run(args, _taskToken.Token); - _logger.LogInformation("Task finished: {Task}", task.Name); + await RunTask(task, arguments); } catch (Exception e) { - _logger.LogError("An unhandled exception occured while running the task {Task}.\n" + - "Inner exception: {Exception}\n\n", task.Name, e.Message); + _logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name); } } else @@ -139,14 +123,51 @@ namespace Kyoo.Controllers } } + /// + /// Parse parameters, inject a task and run it. + /// + /// The task to run + /// The arguments to pass to the function + /// There was an invalid argument or a required argument was not found. + private async Task RunTask(ITask task, Dictionary arguments) + { + _logger.LogInformation("Task starting: {Task}", task.Name); + + ICollection all = task.GetParameters(); + + ICollection invalids = arguments.Keys + .Where(x => all.Any(y => x != y.Name)) + .ToArray(); + if (invalids.Any()) + { + string invalidsStr = string.Join(", ", invalids); + throw new ArgumentException($"{invalidsStr} are invalid arguments for the task {task.Name}"); + } + + TaskParameters args = new(all + .Select(x => + { + object value = arguments + .FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase)) + .Value; + if (value == null && x.IsRequired) + throw new ArgumentException($"The argument {x.Name} is required to run {task.Name}" + + " but it was not specified."); + return x.CreateValue(value ?? x.DefaultValue); + })); + + InjectServices(task); + await task.Run(args, _taskToken.Token); + _logger.LogInformation("Task finished: {Task}", task.Name); + } + /// /// Inject services into the marked properties of the given object. /// /// The object to inject - /// The type of the object. - private void InjectServices(T obj) + private void InjectServices(ITask obj) { - IEnumerable properties = typeof(T).GetProperties() + IEnumerable properties = obj.GetType().GetProperties() .Where(x => x.GetCustomAttribute() != null) .Where(x => x.CanWrite); @@ -180,7 +201,7 @@ namespace Kyoo.Controllers .Where(x => x.RunOnStartup && x.Priority != int.MaxValue) .OrderByDescending(x => x.Priority); foreach (ITask task in startupTasks) - _queuedTasks.Enqueue((task, null)); + _queuedTasks.Enqueue((task, new Dictionary())); } /// diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index beba3b0d..2c73f2db 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Unity; +using Unity.Microsoft.DependencyInjection; namespace Kyoo { @@ -99,7 +100,8 @@ namespace Kyoo if (context.HostingEnvironment.IsDevelopment()) StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); }) - .UseUnityProvider(container) + // .UseUnityProvider(container) + .UseUnityServiceProvider(container) .ConfigureServices(x => x.AddRouting()) .UseKestrel(options => { options.AddServerHeader = false; }) .UseIIS() diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 925d25e1..d0fac3d6 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -3,11 +3,13 @@ using System.IO; using Kyoo.Controllers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Unity; using Unity.Lifetime; @@ -19,11 +21,13 @@ namespace Kyoo public class Startup { private readonly IConfiguration _configuration; + private readonly ILoggerFactory _loggerFactory; - public Startup(IConfiguration configuration) + public Startup(IConfiguration configuration, ILoggerFactory loggerFactory, IServiceProvider provider) { _configuration = configuration; + _loggerFactory = loggerFactory; } public void ConfigureServices(IServiceCollection services) @@ -60,11 +64,20 @@ namespace Kyoo // }); // services.AddAuthentication() + + // container.Resolve(); + services.AddSingleton(); services.AddHostedService(x => x.GetService() as TaskManager); } - public void ConfigureContainer(UnityContainer container) { } + public void ConfigureContainer(IUnityContainer container) + { + container.RegisterType(new SingletonLifetimeManager()); + container.RegisterInstance(_configuration); + PluginManager pluginManager = new(container, _configuration, _loggerFactory.CreateLogger()); + pluginManager.ReloadPlugins(); + } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) { @@ -103,18 +116,15 @@ namespace Kyoo }); app.UseResponseCompression(); - // app.UseSpa(spa => - // { - // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - // - // if (env.IsDevelopment()) - // spa.UseAngularCliServer("start"); - // }); - // - container.RegisterType(new SingletonLifetimeManager()); - // container.Resolve(); + app.UseSpa(spa => + { + spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); + + if (env.IsDevelopment()) + spa.UseAngularCliServer("start"); + }); + IPluginManager pluginManager = container.Resolve(); - pluginManager.ReloadPlugins(); foreach (IPlugin plugin in pluginManager.GetAllPlugins()) plugin.ConfigureAspNet(app); diff --git a/Kyoo/settings.json b/Kyoo/settings.json index bcfc92d3..8cd453e2 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -18,6 +18,8 @@ "logging": { "logLevel": { "default": "Trace", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", "Kyoo": "Trace" } }, From 3e8dbc84a87c952a0ee1b67330c9f96244f3bdd8 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 4 May 2021 01:25:36 +0200 Subject: [PATCH 11/25] Removing UnityContainer and fixing service loading --- Kyoo.Authentication/AuthenticationModule.cs | 3 +- Kyoo.Common/Controllers/IPlugin.cs | 27 ++------- Kyoo.Common/Controllers/IPluginManager.cs | 23 +++++++- Kyoo.Common/Kyoo.Common.csproj | 8 +-- Kyoo.Common/Module.cs | 40 +++++++------- Kyoo.Postgresql/PostgresModule.cs | 6 +- Kyoo/Controllers/PluginManager.cs | 61 ++++++++++++--------- Kyoo/Controllers/TaskManager.cs | 13 ++--- Kyoo/CoreModule.cs | 42 +++++++------- Kyoo/Kyoo.csproj | 2 - Kyoo/Models/LazyDi.cs | 12 ++++ Kyoo/Program.cs | 9 --- Kyoo/Startup.cs | 58 ++++++++++++-------- Kyoo/UnityExtensions/UnityExtensions.cs | 32 ----------- Kyoo/UnityExtensions/UnityProvider.cs | 30 ---------- 15 files changed, 159 insertions(+), 207 deletions(-) create mode 100644 Kyoo/Models/LazyDi.cs delete mode 100644 Kyoo/UnityExtensions/UnityExtensions.cs delete mode 100644 Kyoo/UnityExtensions/UnityProvider.cs diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 4260512c..969f375d 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -62,7 +62,7 @@ namespace Kyoo.Authentication } /// - public IServiceCollection Configure(IServiceCollection services, ICollection availableTypes) + public void Configure(IServiceCollection services, ICollection availableTypes) { string publicUrl = _configuration.GetValue("public_url"); @@ -148,7 +148,6 @@ namespace Kyoo.Authentication AllowedOrigins = {new Uri(publicUrl).GetLeftPart(UriPartial.Authority)} }; services.AddSingleton(cors); - return services; } /// diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index e9776d36..c535992d 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -4,7 +4,6 @@ using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Unity; namespace Kyoo.Controllers { @@ -59,30 +58,14 @@ namespace Kyoo.Controllers /// /// A configure method that will be run on plugin's startup. /// - /// A unity container to register new services. + /// A service container to register new services. /// The list of types that are available for this instance. This can be used /// for conditional type. See - /// or > - void Configure(IUnityContainer container, ICollection availableTypes) {} + /// or > + /// You can't simply check on the service collection because some dependencies might be registered after your plugin. + /// + void Configure(IServiceCollection services, ICollection availableTypes); - /// - /// An optional configure method that will be run on plugin's startup. - /// This method may be used instead or with the - /// method - /// if you use a library that configure itself with a . - /// Every service put in this container will be registered to the unity container after this method. - /// - /// An empty service container to register new services. - /// The list of types that are available for this instance. This can be used - /// for conditional type. See - /// or > - /// You should return the parameter or another container if you want. - /// This container will be added to Kyoo's unity container. - IServiceCollection Configure(IServiceCollection services, ICollection availableTypes) - { - return services; - } - /// /// An optional configuration step to allow a plugin to change asp net configurations. /// WARNING: This is only called on Kyoo's startup so you must restart the app to apply this changes. diff --git a/Kyoo.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs index 558f86bf..bd4ef513 100644 --- a/Kyoo.Common/Controllers/IPluginManager.cs +++ b/Kyoo.Common/Controllers/IPluginManager.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { @@ -29,11 +31,26 @@ namespace Kyoo.Controllers /// /// All plugins currently loaded. public ICollection GetAllPlugins(); + + /// + /// Load plugins and their dependencies from the plugin directory. + /// + /// + /// An initial plugin list to use. + /// You should not try to put plugins from the plugins directory here as they will get automatically loaded. + /// + public void LoadPlugins(ICollection plugins); + + /// + /// Configure services adding or removing services as the plugins wants. + /// + /// The service collection to populate + public void ConfigureServices(IServiceCollection services); /// - /// Load new plugins from the plugin directory. + /// Configure an asp net application applying plugins policies. /// - /// If a plugin can't be loaded because a dependency can't be resolved. - public void ReloadPlugins(); + /// The asp net application to configure + public void ConfigureAspnet(IApplicationBuilder app); } } \ No newline at end of file diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 7211beef..349ef6a0 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -24,14 +24,8 @@ + - - - - - - ..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\5.0.5\Microsoft.Extensions.DependencyInjection.Abstractions.dll - diff --git a/Kyoo.Common/Module.cs b/Kyoo.Common/Module.cs index 5ce34cbe..c1c09165 100644 --- a/Kyoo.Common/Module.cs +++ b/Kyoo.Common/Module.cs @@ -1,6 +1,6 @@ using System; using Kyoo.Controllers; -using Unity; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo { @@ -12,55 +12,55 @@ namespace Kyoo /// /// Register a new task to the container. /// - /// The container + /// The container /// The type of the task /// The initial container. - public static IUnityContainer RegisterTask(this IUnityContainer container) + public static IServiceCollection AddTask(this IServiceCollection services) where T : class, ITask { - container.RegisterType(); - return container; + services.AddSingleton(); + return services; } /// /// Register a new repository to the container. /// - /// The container + /// The container + /// The lifetime of the repository. The default is scoped. /// The type of the repository. /// - /// If your repository implements a special interface, please use + /// If your repository implements a special interface, please use /// /// The initial container. - public static IUnityContainer RegisterRepository(this IUnityContainer container) + public static IServiceCollection AddRepository(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) where T : IBaseRepository { Type repository = Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)); if (repository != null) - { - container.RegisterType(repository, typeof(T)); - container.RegisterType(repository.FriendlyName()); - } - else - container.RegisterType(typeof(T).FriendlyName()); - return container; + services.Add(ServiceDescriptor.Describe(repository, typeof(T), lifetime)); + services.Add(ServiceDescriptor.Describe(typeof(IBaseRepository), typeof(T), lifetime)); + return services; } /// /// Register a new repository with a custom mapping to the container. /// - /// + /// + /// The lifetime of the repository. The default is scoped. /// The custom mapping you have for your repository. /// The type of the repository. /// - /// If your repository does not implements a special interface, please use + /// If your repository does not implements a special interface, please use /// /// The initial container. - public static IUnityContainer RegisterRepository(this IUnityContainer container) + public static IServiceCollection AddRepository(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) where T2 : IBaseRepository, T { - container.RegisterType(); - return container.RegisterRepository(); + services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime)); + return services.AddRepository(lifetime); } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index f56dcac4..52453540 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using Kyoo.Controllers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Unity; namespace Kyoo.Postgresql { @@ -57,9 +57,9 @@ namespace Kyoo.Postgresql } /// - public void Configure(IUnityContainer container, ICollection availableTypes) + public void Configure(IServiceCollection services, ICollection availableTypes) { - container.RegisterFactory(_ => new PostgresContext( + services.AddScoped(_ => new PostgresContext( _configuration.GetDatabaseConnection("postgres"), _environment.IsDevelopment())); } diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 7c1be980..27a5b4bc 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -5,11 +5,10 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; using Kyoo.Models.Exceptions; -using Kyoo.UnityExtensions; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Unity; namespace Kyoo.Controllers { @@ -20,9 +19,9 @@ namespace Kyoo.Controllers public class PluginManager : IPluginManager { /// - /// The unity container. It is given to the Configure method of plugins. + /// The service provider. It allow plugin's activation. /// - private readonly IUnityContainer _container; + private readonly IServiceProvider _provider; /// /// The configuration to get the plugin's directory. /// @@ -40,14 +39,14 @@ namespace Kyoo.Controllers /// /// Create a new instance. /// - /// A unity container to allow plugins to register new entries + /// A service container to allow initialization of plugins /// The configuration instance, to get the plugin's directory path. /// The logger used by this class. - public PluginManager(IUnityContainer container, + public PluginManager(IServiceProvider provider, IConfiguration config, ILogger logger) { - _container = container; + _provider = provider; _config = config; _logger = logger; } @@ -72,7 +71,7 @@ namespace Kyoo.Controllers } /// - public void ReloadPlugins() + public void LoadPlugins(ICollection plugins) { string pluginFolder = _config.GetValue("plugins"); if (!Directory.Exists(pluginFolder)) @@ -80,7 +79,7 @@ namespace Kyoo.Controllers _logger.LogTrace("Loading new plugins..."); string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories); - ICollection newPlugins = pluginsPaths.SelectMany(path => + plugins = pluginsPaths.SelectMany(path => { path = Path.GetFullPath(path); try @@ -90,7 +89,7 @@ namespace Kyoo.Controllers return assembly.GetTypes() .Where(x => typeof(IPlugin).IsAssignableFrom(x)) .Where(x => _plugins.All(y => y.GetType() != x)) - .Select(x => (IPlugin)_container.Resolve(x)) + .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) .ToArray(); } catch (Exception ex) @@ -98,32 +97,40 @@ namespace Kyoo.Controllers _logger.LogError(ex, "Could not load the plugin at {Path}", path); return Array.Empty(); } - }).ToList(); - if (!_plugins.Any()) - newPlugins.Add(new CoreModule()); - _plugins.AddRange(newPlugins); + }).Concat(plugins).ToList(); ICollection available = GetProvidedTypes(); - foreach (IPlugin plugin in newPlugins) + _plugins.AddRange(plugins.Where(plugin => { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); - if (missing != null) - { - Exception error = new MissingDependencyException(plugin.Name, missing.Name); - _logger.LogCritical(error, "A plugin's dependency could not be met"); - } - else - { - plugin.Configure(_container, available); - _container.AddServices(plugin.Configure(new ServiceCollection(), available)); - } - } - + if (missing == null) + return true; + + Exception error = new MissingDependencyException(plugin.Name, missing.Name); + _logger.LogCritical(error, "A plugin's dependency could not be met"); + return false; + })); + if (!_plugins.Any()) _logger.LogInformation("No plugin enabled"); else _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); } + + /// + public void ConfigureServices(IServiceCollection services) + { + ICollection available = GetProvidedTypes(); + foreach (IPlugin plugin in _plugins) + plugin.Configure(services, available); + } + + /// + public void ConfigureAspnet(IApplicationBuilder app) + { + foreach (IPlugin plugin in _plugins) + plugin.ConfigureAspNet(app); + } /// /// Get the list of types provided by the currently loaded plugins. diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 437b8b14..7ba56a07 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -9,7 +9,6 @@ using Kyoo.Models.Exceptions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Unity; namespace Kyoo.Controllers { @@ -22,7 +21,7 @@ namespace Kyoo.Controllers /// /// The service provider used to activate /// - private readonly IUnityContainer _container; + private readonly IServiceProvider _provider; /// /// The configuration instance used to get schedule information /// @@ -54,15 +53,15 @@ namespace Kyoo.Controllers /// Create a new . /// /// The list of tasks to manage - /// The service provider to request services for tasks + /// The service provider to request services for tasks /// The configuration to load schedule information. /// The logger. public TaskManager(IEnumerable tasks, - IUnityContainer container, + IServiceProvider provider, IConfiguration configuration, ILogger logger) { - _container = container; + _provider = provider; _configuration = configuration.GetSection("scheduledTasks"); _logger = logger; _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); @@ -173,7 +172,7 @@ namespace Kyoo.Controllers foreach (PropertyInfo property in properties) { - object value = _container.Resolve(property.PropertyType); + object value = _provider.GetService(property.PropertyType); property.SetValue(obj, value); } } @@ -244,7 +243,7 @@ namespace Kyoo.Controllers /// public void ReloadTasks() { - _tasks = _container.Resolve>().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + // _tasks = _provider.Resolve>().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); EnqueueStartupTasks(); } } diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index 9f9810cf..8bad71e9 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; using Kyoo.Controllers; using Kyoo.Tasks; -using Unity; -using Unity.Lifetime; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo { @@ -52,32 +51,33 @@ namespace Kyoo public ICollection Requires => ArraySegment.Empty; /// - public void Configure(IUnityContainer container, ICollection availableTypes) + public void Configure(IServiceCollection services, ICollection availableTypes) { - container.RegisterType(new SingletonLifetimeManager()); - container.RegisterType(new SingletonLifetimeManager()); - container.RegisterType(new SingletonLifetimeManager()); - container.RegisterType(new SingletonLifetimeManager()); - container.RegisterType(new SingletonLifetimeManager()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(x => x.GetService() as TaskManager); - container.RegisterType(new HierarchicalLifetimeManager()); + services.AddScoped(); if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes)) { - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); } - container.RegisterTask(); + services.AddTask(); } } } \ No newline at end of file diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5eaff52e..ca011593 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -47,8 +47,6 @@ - - diff --git a/Kyoo/Models/LazyDi.cs b/Kyoo/Models/LazyDi.cs new file mode 100644 index 00000000..477e1ec4 --- /dev/null +++ b/Kyoo/Models/LazyDi.cs @@ -0,0 +1,12 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Models +{ + public class LazyDi : Lazy + { + public LazyDi(IServiceProvider provider) + : base(provider.GetRequiredService) + { } + } +} \ No newline at end of file diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 2c73f2db..34ae9206 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -1,16 +1,12 @@ using System; using System.IO; using System.Threading.Tasks; -using Kyoo.UnityExtensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Unity; -using Unity.Microsoft.DependencyInjection; - namespace Kyoo { /// @@ -80,9 +76,6 @@ namespace Kyoo /// A new web host instance private static IWebHostBuilder CreateWebHostBuilder(string[] args) { - UnityContainer container = new(); - container.EnableDebugDiagnostic(); - return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build()) @@ -100,8 +93,6 @@ namespace Kyoo if (context.HostingEnvironment.IsDevelopment()) StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); }) - // .UseUnityProvider(container) - .UseUnityServiceProvider(container) .ConfigureServices(x => x.AddRouting()) .UseKestrel(options => { options.AddServerHeader = false; }) .UseIIS() diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index d0fac3d6..730d4b37 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Kyoo.Controllers; +using Kyoo.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SpaServices.AngularCli; @@ -10,8 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Unity; -using Unity.Lifetime; namespace Kyoo { @@ -20,16 +19,38 @@ namespace Kyoo /// public class Startup { + /// + /// The configuration context + /// private readonly IConfiguration _configuration; - private readonly ILoggerFactory _loggerFactory; + /// + /// A plugin manager used to load plugins and allow them to configure services / asp net. + /// + private readonly IPluginManager _plugins; - public Startup(IConfiguration configuration, ILoggerFactory loggerFactory, IServiceProvider provider) + /// + /// Created from the DI container, those services are needed to load information and instantiate plugins.s + /// + /// + /// The ServiceProvider used to create this instance. + /// The host provider that contains only well-known services that are Kyoo independent. + /// This is used to instantiate plugins that might need a logger, a configuration or an host environment. + /// + /// The configuration context + /// A logger factory used to create a logger for the plugin manager. + public Startup(IServiceProvider hostProvider, IConfiguration configuration, ILoggerFactory loggerFactory) { _configuration = configuration; - _loggerFactory = loggerFactory; + _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); + + _plugins.LoadPlugins(new [] {new CoreModule()}); } + /// + /// Configure the WebApp services context. + /// + /// The service collection to fill. public void ConfigureServices(IServiceCollection services) { string publicUrl = _configuration.GetValue("public_url"); @@ -63,23 +84,18 @@ namespace Kyoo // }); // }); // services.AddAuthentication() - - // container.Resolve(); - - services.AddSingleton(); - services.AddHostedService(x => x.GetService() as TaskManager); - } - - public void ConfigureContainer(IUnityContainer container) - { - container.RegisterType(new SingletonLifetimeManager()); - container.RegisterInstance(_configuration); - PluginManager pluginManager = new(container, _configuration, _loggerFactory.CreateLogger()); - pluginManager.ReloadPlugins(); + services.AddSingleton(_plugins); + services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>)); + _plugins.ConfigureServices(services); } - public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) + /// + /// Configure the asp net host. + /// + /// The asp net host to configure + /// The host environment (is the app in development mode?) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); @@ -124,9 +140,7 @@ namespace Kyoo spa.UseAngularCliServer("start"); }); - IPluginManager pluginManager = container.Resolve(); - foreach (IPlugin plugin in pluginManager.GetAllPlugins()) - plugin.ConfigureAspNet(app); + _plugins.ConfigureAspnet(app); app.UseEndpoints(endpoints => { diff --git a/Kyoo/UnityExtensions/UnityExtensions.cs b/Kyoo/UnityExtensions/UnityExtensions.cs deleted file mode 100644 index b351ec8e..00000000 --- a/Kyoo/UnityExtensions/UnityExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Reflection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Unity; -using Unity.Microsoft.DependencyInjection; - -namespace Kyoo.UnityExtensions -{ - public static class UnityExtensions - { - public static IWebHostBuilder UseUnityProvider(this IWebHostBuilder host, UnityContainer container) - { - UnityProvider factory = new(container); - - return host.ConfigureServices((_, services) => - { - services.Replace(ServiceDescriptor.Singleton>(factory)); - services.Replace(ServiceDescriptor.Singleton>(factory)); - services.Replace(ServiceDescriptor.Singleton>(factory)); - }); - } - - public static IUnityContainer AddServices(this IUnityContainer container, IServiceCollection services) - { - return (IUnityContainer)typeof(ServiceProviderExtensions).Assembly - .GetType("Unity.Microsoft.DependencyInjection.Configuration") - !.GetMethod("AddServices", BindingFlags.Static | BindingFlags.NonPublic) - !.Invoke(null, new object[] {container, services}); - } - } -} \ No newline at end of file diff --git a/Kyoo/UnityExtensions/UnityProvider.cs b/Kyoo/UnityExtensions/UnityProvider.cs deleted file mode 100644 index 93d460f0..00000000 --- a/Kyoo/UnityExtensions/UnityProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Unity; -using Unity.Microsoft.DependencyInjection; - -namespace Kyoo.UnityExtensions -{ - public class UnityProvider : ServiceProviderFactory, IServiceProviderFactory - { - private readonly UnityContainer _container; - - - public UnityProvider(UnityContainer container) - : base(container) - { - _container = container; - } - - public UnityContainer CreateBuilder(IServiceCollection services) - { - _container.AddServices(services); - return _container; - } - - public IServiceProvider CreateServiceProvider(UnityContainer containerBuilder) - { - return CreateServiceProvider(containerBuilder as IUnityContainer); - } - } -} \ No newline at end of file From 709e12191ddc522dd95029f18b38cc640933e4c6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 4 May 2021 15:16:59 +0200 Subject: [PATCH 12/25] Cleaning plugin requirements --- .../Exceptions/MissingDependencyException.cs | 20 ------------------- Kyoo/Controllers/PluginManager.cs | 5 ++--- Kyoo/CoreModule.cs | 15 +++++++++++++- 3 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 Kyoo.Common/Models/Exceptions/MissingDependencyException.cs diff --git a/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs b/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs deleted file mode 100644 index b2a4aa43..00000000 --- a/Kyoo.Common/Models/Exceptions/MissingDependencyException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Kyoo.Models.Exceptions -{ - /// - /// An exception raised when a plugin requires dependencies that can't be found with the current configuration. - /// - [Serializable] - public class MissingDependencyException : Exception - { - /// - /// Create a new with a custom message - /// - /// The name of the plugin that can't be loaded. - /// The name of the missing dependency. - public MissingDependencyException(string plugin, string dependency) - : base($"No {dependency} are available in Kyoo but the plugin {plugin} requires it.") - {} - } -} \ No newline at end of file diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 27a5b4bc..eaf08b97 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -106,8 +105,8 @@ namespace Kyoo.Controllers if (missing == null) return true; - Exception error = new MissingDependencyException(plugin.Name, missing.Name); - _logger.LogCritical(error, "A plugin's dependency could not be met"); + _logger.LogCritical("No {Dependency} available in Kyoo but the plugin {Plugin} requires it", + missing.Name, plugin.Name); return false; })); diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index 8bad71e9..f7d5dea5 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -48,7 +48,20 @@ namespace Kyoo }; /// - public ICollection Requires => ArraySegment.Empty; + public ICollection Requires => new [] + { + typeof(ILibraryRepository), + typeof(ILibraryItemRepository), + typeof(ICollectionRepository), + typeof(IShowRepository), + typeof(ISeasonRepository), + typeof(IEpisodeRepository), + typeof(ITrackRepository), + typeof(IPeopleRepository), + typeof(IStudioRepository), + typeof(IGenreRepository), + typeof(IProviderRepository) + }; /// public void Configure(IServiceCollection services, ICollection availableTypes) From feb643da2edb38ed094e9964c4afd76539d19002 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 4 May 2021 17:59:05 +0200 Subject: [PATCH 13/25] Fixing database configuration There is still broken dependencies when the module is in another assembly --- .../Kyoo.Authentication.csproj | 12 ++-- Kyoo.CommonAPI/DatabaseContext.cs | 14 ++++ Kyoo.CommonAPI/Kyoo.CommonAPI.csproj | 5 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 6 +- Kyoo.Postgresql/PostgresContext.cs | 29 ++++++-- Kyoo.Postgresql/PostgresModule.cs | 14 +++- Kyoo.Tests/Kyoo.Tests.csproj | 6 +- Kyoo/Controllers/PluginManager.cs | 67 ++++++++++++------- Kyoo/Kyoo.csproj | 16 ++--- Kyoo/Startup.cs | 19 +++--- 10 files changed, 120 insertions(+), 68 deletions(-) diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj index 29c2a08a..17107035 100644 --- a/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -16,13 +16,13 @@ - - - - - + + + + + - + diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index b58710f3..d92c208f 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -84,6 +84,20 @@ namespace Kyoo return Set>(); } + + /// + /// The default constructor + /// + protected DatabaseContext() { } + + /// + /// Create a new using specific options + /// + /// The options to use. + protected DatabaseContext(DbContextOptions options) + : base(options) + { } + /// /// Set basic configurations (like preventing query tracking) /// diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index 3a2b6456..5288b814 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,10 +12,9 @@ - - + + - diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 65f2f5e5..10887b80 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -16,11 +16,7 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index ea220cfc..36ca803e 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -21,6 +21,11 @@ namespace Kyoo.Postgresql /// Is this instance in debug mode? /// private readonly bool _debugMode; + + /// + /// Should the configure step be skipped? This is used when the database is created via DbContextOptions. + /// + private readonly bool _skipConfigure; /// /// A basic constructor that set default values (query tracker behaviors, mapping enums...) @@ -32,6 +37,19 @@ namespace Kyoo.Postgresql NpgsqlConnection.GlobalTypeMapper.MapEnum(); } + /// + /// Create a new using specific options + /// + /// The options to use. + public PostgresContext(DbContextOptions options) + : base(options) + { + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + _skipConfigure = true; + } + /// /// A basic constructor that set default values (query tracker behaviors, mapping enums...) /// @@ -49,10 +67,13 @@ namespace Kyoo.Postgresql /// An option builder to fill. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.UseNpgsql(_connection); - if (_debugMode) - optionsBuilder.EnableDetailedErrors() - .EnableSensitiveDataLogging(); + if (!_skipConfigure) + { + optionsBuilder.UseNpgsql(_connection); + if (_debugMode) + optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); + } + base.OnConfiguring(optionsBuilder); } diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 52453540..df829fdd 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Kyoo.Controllers; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -59,9 +60,16 @@ namespace Kyoo.Postgresql /// public void Configure(IServiceCollection services, ICollection availableTypes) { - services.AddScoped(_ => new PostgresContext( - _configuration.GetDatabaseConnection("postgres"), - _environment.IsDevelopment())); + services.AddDbContext(x => + { + x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); + if (_environment.IsDevelopment()) + x.EnableDetailedErrors().EnableSensitiveDataLogging(); + }); + // services.AddScoped(_ => new PostgresContext( + // _configuration.GetDatabaseConnection("postgres"), + // _environment.IsDevelopment())); + // services.AddScoped(x => x.GetRequiredService()); } } } \ No newline at end of file diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 65e01c1f..b5e3dd82 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -14,14 +14,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index eaf08b97..c17e3230 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -69,6 +69,31 @@ namespace Kyoo.Controllers return _plugins; } + /// + /// Load a single plugin and return all IPlugin implementations contained in the Assembly. + /// + /// The path of the dll + /// The list of dlls in hte assembly + private IPlugin[] LoadPlugin(string path) + { + path = Path.GetFullPath(path); + try + { + PluginDependencyLoader loader = new(path); + Assembly assembly = loader.LoadFromAssemblyPath(path); + return assembly.GetTypes() + .Where(x => typeof(IPlugin).IsAssignableFrom(x)) + .Where(x => _plugins.All(y => y.GetType() != x)) + .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) + .ToArray(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not load the plugin at {Path}", path); + return Array.Empty(); + } + } + /// public void LoadPlugins(ICollection plugins) { @@ -78,27 +103,12 @@ namespace Kyoo.Controllers _logger.LogTrace("Loading new plugins..."); string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories); - plugins = pluginsPaths.SelectMany(path => - { - path = Path.GetFullPath(path); - try - { - PluginDependencyLoader loader = new(path); - Assembly assembly = loader.LoadFromAssemblyPath(path); - return assembly.GetTypes() - .Where(x => typeof(IPlugin).IsAssignableFrom(x)) - .Where(x => _plugins.All(y => y.GetType() != x)) - .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) - .ToArray(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not load the plugin at {Path}", path); - return Array.Empty(); - } - }).Concat(plugins).ToList(); + plugins = plugins.Concat(pluginsPaths.SelectMany(LoadPlugin)) + .GroupBy(x => x.Name) + .Select(x => x.First()) + .ToList(); - ICollection available = GetProvidedTypes(); + ICollection available = GetProvidedTypes(plugins); _plugins.AddRange(plugins.Where(plugin => { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); @@ -119,7 +129,7 @@ namespace Kyoo.Controllers /// public void ConfigureServices(IServiceCollection services) { - ICollection available = GetProvidedTypes(); + ICollection available = GetProvidedTypes(_plugins); foreach (IPlugin plugin in _plugins) plugin.Configure(services, available); } @@ -134,11 +144,12 @@ namespace Kyoo.Controllers /// /// Get the list of types provided by the currently loaded plugins. /// + /// The list of plugins that will be used as a plugin pool to get provided types. /// The list of types available. - private ICollection GetProvidedTypes() + private ICollection GetProvidedTypes(ICollection plugins) { - List available = _plugins.SelectMany(x => x.Provides).ToList(); - List conditionals =_plugins + List available = plugins.SelectMany(x => x.Provides).ToList(); + List conditionals = plugins .SelectMany(x => x.ConditionalProvides) .Where(x => x.Condition.Condition()) .ToList(); @@ -199,6 +210,14 @@ namespace Kyoo.Controllers /// protected override Assembly Load(AssemblyName assemblyName) { + Assembly existing = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => + { + AssemblyName name = x.GetName(); + return name.Name == assemblyName.Name && name.Version == assemblyName.Version; + }); + if (existing != null) + return existing; string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) return LoadFromAssemblyPath(assemblyPath); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index ca011593..d4e376b4 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -36,17 +36,11 @@ - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - + + + + + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 730d4b37..c267bd00 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -2,6 +2,7 @@ using System; using System.IO; using Kyoo.Controllers; using Kyoo.Models; +using Kyoo.Postgresql; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SpaServices.AngularCli; @@ -39,12 +40,12 @@ namespace Kyoo /// /// The configuration context /// A logger factory used to create a logger for the plugin manager. - public Startup(IServiceProvider hostProvider, IConfiguration configuration, ILoggerFactory loggerFactory) + public Startup(IServiceProvider hostProvider, IConfiguration configuration, ILoggerFactory loggerFactory, IWebHostEnvironment host) { _configuration = configuration; _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); - _plugins.LoadPlugins(new [] {new CoreModule()}); + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); } /// @@ -132,13 +133,13 @@ namespace Kyoo }); app.UseResponseCompression(); - app.UseSpa(spa => - { - spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - - if (env.IsDevelopment()) - spa.UseAngularCliServer("start"); - }); + // app.UseSpa(spa => + // { + // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); + // + // if (env.IsDevelopment()) + // spa.UseAngularCliServer("start"); + // }); _plugins.ConfigureAspnet(app); From b2151a0dde56a4717966e4aa88ca61f98b9d9994 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 5 May 2021 20:49:52 +0200 Subject: [PATCH 14/25] Fixing postgres migrations --- Kyoo.Common/Controllers/IPlugin.cs | 7 + Kyoo.Common/Controllers/ITask.cs | 4 +- Kyoo.Common/Controllers/ITaskManager.cs | 5 - .../Models/Attributes/InjectedAttribute.cs | 2 + Kyoo.Postgresql/Kyoo.Postgresql.csproj | 8 +- .../20210505182627_Initial.Designer.cs | 786 ++++++++++++++++++ .../Migrations/20210505182627_Initial.cs | 608 ++++++++++++++ .../PostgresContextModelSnapshot.cs | 784 +++++++++++++++++ Kyoo.Postgresql/PostgresContext.cs | 5 +- Kyoo.Postgresql/PostgresModule.cs | 7 + Kyoo/Controllers/TaskManager.cs | 45 +- Kyoo/Startup.cs | 4 +- Kyoo/Tasks/PluginInitializer.cs | 60 ++ 13 files changed, 2287 insertions(+), 38 deletions(-) create mode 100644 Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs create mode 100644 Kyoo.Postgresql/Migrations/20210505182627_Initial.cs create mode 100644 Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs create mode 100644 Kyoo/Tasks/PluginInitializer.cs diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index c535992d..3201df83 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -72,6 +72,13 @@ namespace Kyoo.Controllers /// /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities. void ConfigureAspNet(IApplicationBuilder app) {} + + /// + /// An optional function to execute and initialize your plugin. + /// It can be used to initialize a database connection, fill initial data or anything. + /// + /// A service provider to request services + void Initialize(IServiceProvider provider) {} } /// diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs index 6ac064e9..75277dd2 100644 --- a/Kyoo.Common/Controllers/ITask.cs +++ b/Kyoo.Common/Controllers/ITask.cs @@ -151,7 +151,7 @@ namespace Kyoo.Controllers public string HelpMessage { get; } /// - /// Should this task be automatically runned at app startup? + /// Should this task be automatically run at app startup? /// public bool RunOnStartup { get; } @@ -165,7 +165,7 @@ namespace Kyoo.Controllers /// Start this task. /// /// The list of parameters. - /// A token to request the task's cancelation. + /// A token to request the task's cancellation. /// If this task is not cancelled quickly, it might be killed by the runner. /// /// Your task can have any service as a public field and use the , diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs index 5a0710c6..392355d3 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -30,10 +30,5 @@ namespace Kyoo.Controllers /// /// A list of every tasks that this instance know. ICollection GetAllTasks(); - - /// - /// Reload tasks and run startup tasks. - /// - void ReloadTasks(); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs index af76eb88..1e9a8ece 100644 --- a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Kyoo.Controllers; namespace Kyoo.Models.Attributes @@ -10,5 +11,6 @@ namespace Kyoo.Models.Attributes /// It should only be used on and will be injected before calling /// [AttributeUsage(AttributeTargets.Property)] + [MeansImplicitUse(ImplicitUseKindFlags.Assign)] public class InjectedAttribute : Attribute { } } \ No newline at end of file diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 10887b80..56479e95 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -16,17 +16,17 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - all - false - all - false diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs new file mode 100644 index 00000000..20ac842f --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs @@ -0,0 +1,786 @@ +// +using System; +using Kyoo.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20210505182627_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) + .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) + .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AbsoluteNumber") + .HasColumnType("integer"); + + b.Property("EpisodeNumber") + .HasColumnType("integer"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Runtime") + .HasColumnType("integer"); + + b.Property("SeasonID") + .HasColumnType("integer"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Thumb") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Paths") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("EpisodeID") + .HasColumnType("integer"); + + b.Property("Link") + .HasColumnType("text"); + + b.Property("PeopleID") + .HasColumnType("integer"); + + b.Property("ProviderID") + .HasColumnType("integer"); + + b.Property("SeasonID") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ProviderID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID"); + + b.ToTable("MetadataIds"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("PeopleID") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("LogoExtension") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Aliases") + .HasColumnType("text[]"); + + b.Property("Backdrop") + .HasColumnType("text"); + + b.Property("EndYear") + .HasColumnType("integer"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartYear") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("status"); + + b.Property("StudioID") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrailerUrl") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("EpisodeID") + .HasColumnType("integer"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrackIndex") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("stream_type"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID"); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("ExternalIDs") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.People", "People") + .WithMany("ExternalIDs") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Provider", "Provider") + .WithMany("MetadataLinks") + .HasForeignKey("ProviderID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("ExternalIDs") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("ExternalIDs") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs b/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs new file mode 100644 index 00000000..eacd436d --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs @@ -0,0 +1,608 @@ +using System; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:item_type", "show,movie,collection") + .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); + + migrationBuilder.CreateTable( + name: "Collections", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Poster = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Collections", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Genres", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Genres", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Libraries", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Paths = table.Column(type: "text[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Libraries", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "People", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_People", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Providers", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Logo = table.Column(type: "text", nullable: true), + LogoExtension = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Providers", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Studios", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Studios", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Collections_SecondID", + column: x => x.SecondID, + principalTable: "Collections", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Shows", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Title = table.Column(type: "text", nullable: true), + Aliases = table.Column(type: "text[]", nullable: true), + Path = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + Status = table.Column(type: "status", nullable: true), + TrailerUrl = table.Column(type: "text", nullable: true), + StartYear = table.Column(type: "integer", nullable: true), + EndYear = table.Column(type: "integer", nullable: true), + Poster = table.Column(type: "text", nullable: true), + Logo = table.Column(type: "text", nullable: true), + Backdrop = table.Column(type: "text", nullable: true), + IsMovie = table.Column(type: "boolean", nullable: false), + StudioID = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Shows", x => x.ID); + table.ForeignKey( + name: "FK_Shows_Studios_StudioID", + column: x => x.StudioID, + principalTable: "Studios", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Collections_FirstID", + column: x => x.FirstID, + principalTable: "Collections", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Genres_SecondID", + column: x => x.SecondID, + principalTable: "Genres", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_FirstID", + column: x => x.FirstID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PeopleRoles", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PeopleID = table.Column(type: "integer", nullable: false), + ShowID = table.Column(type: "integer", nullable: false), + Role = table.Column(type: "text", nullable: true), + Type = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PeopleRoles", x => x.ID); + table.ForeignKey( + name: "FK_PeopleRoles_People_PeopleID", + column: x => x.PeopleID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PeopleRoles_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Seasons", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ShowID = table.Column(type: "integer", nullable: false), + SeasonNumber = table.Column(type: "integer", nullable: false), + Title = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + Year = table.Column(type: "integer", nullable: true), + Poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Seasons", x => x.ID); + table.ForeignKey( + name: "FK_Seasons_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Episodes", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ShowID = table.Column(type: "integer", nullable: false), + SeasonID = table.Column(type: "integer", nullable: true), + SeasonNumber = table.Column(type: "integer", nullable: false), + EpisodeNumber = table.Column(type: "integer", nullable: false), + AbsoluteNumber = table.Column(type: "integer", nullable: false), + Path = table.Column(type: "text", nullable: true), + Thumb = table.Column(type: "text", nullable: true), + Title = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true), + Runtime = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Episodes", x => x.ID); + table.ForeignKey( + name: "FK_Episodes_Seasons_SeasonID", + column: x => x.SeasonID, + principalTable: "Seasons", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Episodes_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MetadataIds", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProviderID = table.Column(type: "integer", nullable: false), + ShowID = table.Column(type: "integer", nullable: true), + EpisodeID = table.Column(type: "integer", nullable: true), + SeasonID = table.Column(type: "integer", nullable: true), + PeopleID = table.Column(type: "integer", nullable: true), + DataID = table.Column(type: "text", nullable: true), + Link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataIds", x => x.ID); + table.ForeignKey( + name: "FK_MetadataIds_Episodes_EpisodeID", + column: x => x.EpisodeID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_People_PeopleID", + column: x => x.PeopleID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Providers_ProviderID", + column: x => x.ProviderID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Seasons_SeasonID", + column: x => x.SeasonID, + principalTable: "Seasons", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataIds_Shows_ShowID", + column: x => x.ShowID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tracks", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EpisodeID = table.Column(type: "integer", nullable: false), + TrackIndex = table.Column(type: "integer", nullable: false), + IsDefault = table.Column(type: "boolean", nullable: false), + IsForced = table.Column(type: "boolean", nullable: false), + IsExternal = table.Column(type: "boolean", nullable: false), + Title = table.Column(type: "text", nullable: true), + Language = table.Column(type: "text", nullable: true), + Codec = table.Column(type: "text", nullable: true), + Path = table.Column(type: "text", nullable: true), + Type = table.Column(type: "stream_type", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tracks", x => x.ID); + table.ForeignKey( + name: "FK_Tracks_Episodes_EpisodeID", + column: x => x.EpisodeID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Collections_Slug", + table: "Collections", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Episodes_SeasonID", + table: "Episodes", + column: "SeasonID"); + + migrationBuilder.CreateIndex( + name: "IX_Episodes_ShowID_SeasonNumber_EpisodeNumber_AbsoluteNumber", + table: "Episodes", + columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Genres_Slug", + table: "Genres", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Libraries_Slug", + table: "Libraries", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_EpisodeID", + table: "MetadataIds", + column: "EpisodeID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_PeopleID", + table: "MetadataIds", + column: "PeopleID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_ProviderID", + table: "MetadataIds", + column: "ProviderID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_SeasonID", + table: "MetadataIds", + column: "SeasonID"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataIds_ShowID", + table: "MetadataIds", + column: "ShowID"); + + migrationBuilder.CreateIndex( + name: "IX_People_Slug", + table: "People", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PeopleRoles_PeopleID", + table: "PeopleRoles", + column: "PeopleID"); + + migrationBuilder.CreateIndex( + name: "IX_PeopleRoles_ShowID", + table: "PeopleRoles", + column: "ShowID"); + + migrationBuilder.CreateIndex( + name: "IX_Providers_Slug", + table: "Providers", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Seasons_ShowID_SeasonNumber", + table: "Seasons", + columns: new[] { "ShowID", "SeasonNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shows_Slug", + table: "Shows", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shows_StudioID", + table: "Shows", + column: "StudioID"); + + migrationBuilder.CreateIndex( + name: "IX_Studios_Slug", + table: "Studios", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", + table: "Tracks", + columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "MetadataIds"); + + migrationBuilder.DropTable( + name: "PeopleRoles"); + + migrationBuilder.DropTable( + name: "Tracks"); + + migrationBuilder.DropTable( + name: "Collections"); + + migrationBuilder.DropTable( + name: "Libraries"); + + migrationBuilder.DropTable( + name: "Genres"); + + migrationBuilder.DropTable( + name: "Providers"); + + migrationBuilder.DropTable( + name: "People"); + + migrationBuilder.DropTable( + name: "Episodes"); + + migrationBuilder.DropTable( + name: "Seasons"); + + migrationBuilder.DropTable( + name: "Shows"); + + migrationBuilder.DropTable( + name: "Studios"); + } + } +} diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs new file mode 100644 index 00000000..05d8772d --- /dev/null +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -0,0 +1,784 @@ +// +using System; +using Kyoo.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + partial class PostgresContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) + .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) + .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AbsoluteNumber") + .HasColumnType("integer"); + + b.Property("EpisodeNumber") + .HasColumnType("integer"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Runtime") + .HasColumnType("integer"); + + b.Property("SeasonID") + .HasColumnType("integer"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Thumb") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Paths") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("DataID") + .HasColumnType("text"); + + b.Property("EpisodeID") + .HasColumnType("integer"); + + b.Property("Link") + .HasColumnType("text"); + + b.Property("PeopleID") + .HasColumnType("integer"); + + b.Property("ProviderID") + .HasColumnType("integer"); + + b.Property("SeasonID") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ProviderID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("ShowID"); + + b.ToTable("MetadataIds"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("PeopleID") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("LogoExtension") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("SeasonNumber") + .HasColumnType("integer"); + + b.Property("ShowID") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("ID"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Aliases") + .HasColumnType("text[]"); + + b.Property("Backdrop") + .HasColumnType("text"); + + b.Property("EndYear") + .HasColumnType("integer"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Poster") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartYear") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("status"); + + b.Property("StudioID") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrailerUrl") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("EpisodeID") + .HasColumnType("integer"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TrackIndex") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("stream_type"); + + b.HasKey("ID"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID"); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("GenreLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("ExternalIDs") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.People", "People") + .WithMany("ExternalIDs") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Provider", "Provider") + .WithMany("MetadataLinks") + .HasForeignKey("ProviderID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("ExternalIDs") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("ExternalIDs") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 36ca803e..b5e4febe 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -69,7 +69,10 @@ namespace Kyoo.Postgresql { if (!_skipConfigure) { - optionsBuilder.UseNpgsql(_connection); + if (_connection != null) + optionsBuilder.UseNpgsql(_connection); + else + optionsBuilder.UseNpgsql(); if (_debugMode) optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); } diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index df829fdd..7a818296 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -71,5 +71,12 @@ namespace Kyoo.Postgresql // _environment.IsDevelopment())); // services.AddScoped(x => x.GetRequiredService()); } + + /// + public void Initialize(IServiceProvider provider) + { + DatabaseContext context = provider.GetRequiredService(); + context.Database.Migrate(); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 7ba56a07..5f281ba3 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -4,9 +4,11 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -34,7 +36,7 @@ namespace Kyoo.Controllers /// /// The list of tasks and their next scheduled run. /// - private List<(ITask task, DateTime scheduledDate)> _tasks; + private readonly List<(ITask task, DateTime scheduledDate)> _tasks; /// /// The queue of tasks that should be run as soon as possible. /// @@ -47,8 +49,8 @@ namespace Kyoo.Controllers /// The cancellation token used to cancel the running task when the runner should shutdown. /// private readonly CancellationTokenSource _taskToken = new(); - - + + /// /// Create a new . /// @@ -64,7 +66,7 @@ namespace Kyoo.Controllers _provider = provider; _configuration = configuration.GetSection("scheduledTasks"); _logger = logger; - _tasks = tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); + _tasks = tasks.Select(x => (x, GetNextTaskDate(x.Slug))).ToList(); if (_tasks.Any()) _logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.task.Name)); @@ -154,9 +156,11 @@ namespace Kyoo.Controllers " but it was not specified."); return x.CreateValue(value ?? x.DefaultValue); })); - - InjectServices(task); + + using IServiceScope scope = _provider.CreateScope(); + InjectServices(task, x => scope.ServiceProvider.GetRequiredService(x)); await task.Run(args, _taskToken.Token); + InjectServices(task, _ => null); _logger.LogInformation("Task finished: {Task}", task.Name); } @@ -164,17 +168,15 @@ namespace Kyoo.Controllers /// Inject services into the marked properties of the given object. /// /// The object to inject - private void InjectServices(ITask obj) + /// The function used to retrieve services. (The function is called immediately) + private static void InjectServices(ITask obj, [InstantHandle] Func retrieve) { IEnumerable properties = obj.GetType().GetProperties() .Where(x => x.GetCustomAttribute() != null) .Where(x => x.CanWrite); foreach (PropertyInfo property in properties) - { - object value = _provider.GetService(property.PropertyType); - property.SetValue(obj, value); - } + property.SetValue(obj, retrieve(property.PropertyType)); } /// @@ -197,7 +199,7 @@ namespace Kyoo.Controllers private void EnqueueStartupTasks() { IEnumerable startupTasks = _tasks.Select(x => x.task) - .Where(x => x.RunOnStartup && x.Priority != int.MaxValue) + .Where(x => x.RunOnStartup) .OrderByDescending(x => x.Priority); foreach (ITask task in startupTasks) _queuedTasks.Enqueue((task, new Dictionary())); @@ -212,20 +214,20 @@ namespace Kyoo.Controllers if (index == -1) throw new ItemNotFoundException($"No task found with the slug {taskSlug}"); _queuedTasks.Enqueue((_tasks[index].task, arguments)); - _tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug)); + _tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug)); } /// - /// Get the delay of a task + /// Get the next date of the execution of the given task. /// /// The slug of the task - /// The delay of the task. - private TimeSpan GetTaskDelay(string taskSlug) + /// The next date. + private DateTime GetNextTaskDate(string taskSlug) { TimeSpan delay = _configuration.GetValue(taskSlug); if (delay == default) - delay = TimeSpan.MaxValue; - return delay; + return DateTime.MaxValue; + return DateTime.Now + delay; } /// @@ -239,12 +241,5 @@ namespace Kyoo.Controllers { return _tasks.Select(x => x.task).ToArray(); } - - /// - public void ReloadTasks() - { - // _tasks = _provider.Resolve>().Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))).ToList(); - EnqueueStartupTasks(); - } } } \ No newline at end of file diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index c267bd00..b60b3e09 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -3,6 +3,7 @@ using System.IO; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Postgresql; +using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SpaServices.AngularCli; @@ -85,9 +86,10 @@ namespace Kyoo // }); // }); // services.AddAuthentication() + services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>)); services.AddSingleton(_plugins); - services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>)); + services.AddTask(); _plugins.ConfigureServices(services); } diff --git a/Kyoo/Tasks/PluginInitializer.cs b/Kyoo/Tasks/PluginInitializer.cs new file mode 100644 index 00000000..55907649 --- /dev/null +++ b/Kyoo/Tasks/PluginInitializer.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models.Attributes; + +namespace Kyoo.Tasks +{ + /// + /// A task run on Kyoo's startup to initialize plugins + /// + public class PluginInitializer : ITask + { + /// + public string Slug => "plugin-init"; + + /// + public string Name => "PluginInitializer"; + + /// + public string Description => "A task to initialize plugins."; + + /// + public string HelpMessage => null; + + /// + public bool RunOnStartup => true; + + /// + public int Priority => int.MaxValue; + + + /// + /// The plugin manager used to retrieve plugins to initialize them. + /// + [Injected] public IPluginManager PluginManager { private get; set; } + /// + /// The service provider given to each method. + /// + [Injected] public IServiceProvider Provider { private get; set; } + + /// + public Task Run(TaskParameters arguments, CancellationToken cancellationToken) + { + foreach (IPlugin plugin in PluginManager.GetAllPlugins()) + plugin.Initialize(Provider); + return Task.CompletedTask; + } + + public TaskParameters GetParameters() + { + return new(); + } + + public int? Progress() + { + return null; + } + } +} \ No newline at end of file From 0f2bea9bc459507d0240a204950b0025d3cacc3f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 6 May 2021 01:40:19 +0200 Subject: [PATCH 15/25] Starting to rework authentication --- Kyoo.Authentication/AuthenticationModule.cs | 16 +- .../Kyoo.Authentication.csproj | 3 - .../Models/DTO/RegisterRequest.cs | 45 +++ Kyoo.Authentication/User.cs | 44 +-- Kyoo.Authentication/Views/AccountApi.cs | 374 +++++++++--------- Kyoo.Common/Models/Resources/IResource.cs | 37 +- Kyoo.Common/Models/Resources/User.cs | 41 ++ Kyoo.Common/Utility.cs | 66 ---- Kyoo.CommonAPI/DatabaseContext.cs | 4 + Kyoo/Startup.cs | 4 +- Kyoo/settings.json | 4 +- 11 files changed, 323 insertions(+), 315 deletions(-) create mode 100644 Kyoo.Authentication/Models/DTO/RegisterRequest.cs create mode 100644 Kyoo.Common/Models/Resources/User.cs diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index e49cee9b..82d05cce 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -64,7 +64,7 @@ namespace Kyoo.Authentication /// public void Configure(IServiceCollection services, ICollection availableTypes) { - string publicUrl = _configuration.GetValue("public_url"); + string publicUrl = _configuration.GetValue("public_url").TrimEnd('/'); // services.AddDbContext(options => // { @@ -86,9 +86,9 @@ namespace Kyoo.Authentication services.AddIdentityServer(options => { options.IssuerUri = publicUrl; - options.UserInteraction.LoginUrl = publicUrl + "login"; - options.UserInteraction.ErrorUrl = publicUrl + "error"; - options.UserInteraction.LogoutUrl = publicUrl + "logout"; + options.UserInteraction.LoginUrl = $"{publicUrl}/login"; + options.UserInteraction.ErrorUrl = $"{publicUrl}/error"; + options.UserInteraction.LogoutUrl = $"{publicUrl}/logout"; }) // .AddAspNetIdentity() // .AddConfigurationStore(options => @@ -105,11 +105,13 @@ namespace Kyoo.Authentication // options.EnableTokenCleanup = true; // }) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) - .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) .AddInMemoryClients(IdentityContext.GetClients()) - // .AddProfileService() - .AddSigninKeys(certificateOptions); + .AddDeveloperSigningCredential(); + // .AddProfileService() + // .AddSigninKeys(certificateOptions); + // TODO implement means to add clients or api scopes for other plugins. + // TODO split scopes (kyoo.read should be task.read, video.read etc) services.AddAuthentication(o => { diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj index 17107035..7bb0a7fb 100644 --- a/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -17,9 +17,6 @@ - - - diff --git a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs new file mode 100644 index 00000000..4d8efa2c --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using Kyoo.Models; + +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A model only used on register requests. + /// + public class RegisterRequest + { + /// + /// The user email address + /// + [EmailAddress] + public string Email { get; set; } + + /// + /// The user's username. + /// + [MinLength(4)] + public string Username { get; set; } + + /// + /// The user's password. + /// + [MinLength(8)] + public string Password { get; set; } + + + /// + /// Convert this register request to a new class. + /// + /// + public User ToUser() + { + return new() + { + Slug = Utility.ToSlug(Username), + Username = Username, + Password = Password, + Email = Email + }; + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/User.cs b/Kyoo.Authentication/User.cs index 2a65d409..896d3004 100644 --- a/Kyoo.Authentication/User.cs +++ b/Kyoo.Authentication/User.cs @@ -1,22 +1,22 @@ -using System; -using IdentityModel; -using Microsoft.AspNetCore.Identity; - -namespace Kyoo.Models -{ - public class User : IdentityUser - { - public string OTAC { get; set; } - public DateTime? OTACExpires { get; set; } - - public string GenerateOTAC(TimeSpan validFor) - { - string otac = CryptoRandom.CreateUniqueId(); - string hashed = otac; // TODO should add a good hashing here. - - OTAC = hashed; - OTACExpires = DateTime.UtcNow.Add(validFor); - return otac; - } - } -} \ No newline at end of file +// using System; +// using IdentityModel; +// using Microsoft.AspNetCore.Identity; +// +// namespace Kyoo.Models +// { +// public class User : IdentityUser +// { +// public string OTAC { get; set; } +// public DateTime? OTACExpires { get; set; } +// +// public string GenerateOTAC(TimeSpan validFor) +// { +// string otac = CryptoRandom.CreateUniqueId(); +// string hashed = otac; // TODO should add a good hashing here. +// +// OTAC = hashed; +// OTACExpires = DateTime.UtcNow.Add(validFor); +// return otac; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 7acf5226..de47d57d 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -1,191 +1,183 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Services; -using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; - -namespace Kyoo.Api -{ - public class RegisterRequest - { - public string Email { get; set; } - public string Username { get; set; } - public string Password { get; set; } - } - - public class LoginRequest - { - public string Username { get; set; } - public string Password { get; set; } - public bool StayLoggedIn { get; set; } - } - - public class OtacRequest - { - public string Otac { get; set; } - public bool StayLoggedIn { get; set; } - } - - public class AccountData - { - [FromForm(Name = "email")] - public string Email { get; set; } - [FromForm(Name = "username")] - public string Username { get; set; } - [FromForm(Name = "picture")] - public IFormFile Picture { get; set; } - } - - - [Route("api/[controller]")] - [ApiController] - public class AccountController : Controller, IProfileService - { - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly IConfiguration _configuration; - private readonly string _picturePath; - - public AccountController(UserManager userManager, - SignInManager siginInManager, - IConfiguration configuration) - { - _userManager = userManager; - _signInManager = siginInManager; - _picturePath = configuration.GetValue("profilePicturePath"); - _configuration = configuration; - if (!Path.IsPathRooted(_picturePath)) - _picturePath = Path.GetFullPath(_picturePath); - } - - [HttpPost("register")] - public async Task Register([FromBody] RegisterRequest user) - { - if (!ModelState.IsValid) - return BadRequest(user); - if (user.Username.Length < 4) - return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}}); - if (!new EmailAddressAttribute().IsValid(user.Email)) - return BadRequest(new[] {new {code = "email", description = "Email must be valid."}}); - User account = new() {UserName = user.Username, Email = user.Email}; - IdentityResult result = await _userManager.CreateAsync(account, user.Password); - if (!result.Succeeded) - return BadRequest(result.Errors); - string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1)); - await _userManager.UpdateAsync(account); - await _userManager.AddClaimAsync(account, new Claim( - "permissions", - _configuration.GetValue("newUserPermissions"))); - return Ok(new {otac}); - } - - [HttpPost("login")] - public async Task Login([FromBody] LoginRequest login) - { - if (!ModelState.IsValid) - return BadRequest(login); - SignInResult result = await _signInManager - .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false); - if (result.Succeeded) - return Ok(); - return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}}); - } - - [HttpPost("otac-login")] - public async Task OtacLogin([FromBody] OtacRequest otac) - { - if (!ModelState.IsValid) - return BadRequest(otac); - User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac); - if (user == null) - return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}}); - if (user.OTACExpires <= DateTime.UtcNow) - return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}}); - await _signInManager.SignInAsync(user, otac.StayLoggedIn); - return Ok(); - } - - [HttpGet("logout")] - [Authorize] - public async Task Logout() - { - await _signInManager.SignOutAsync(); - return Ok(); - } - - public async Task GetProfileDataAsync(ProfileDataRequestContext context) - { - User user = await _userManager.GetUserAsync(context.Subject); - if (user != null) - { - List claims = new() - { - new Claim("email", user.Email), - new Claim("username", user.UserName), - new Claim("picture", $"api/account/picture/{user.UserName}") - }; - - Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); - if (perms != null) - claims.Add(perms); - - context.IssuedClaims.AddRange(claims); - } - } - - public async Task IsActiveAsync(IsActiveContext context) - { - User user = await _userManager.GetUserAsync(context.Subject); - context.IsActive = user != null; - } - - [HttpGet("picture/{username}")] - public async Task GetPicture(string username) - { - User user = await _userManager.FindByNameAsync(username); - if (user == null) - return BadRequest(); - string path = Path.Combine(_picturePath, user.Id); - if (!System.IO.File.Exists(path)) - return NotFound(); - return new PhysicalFileResult(path, "image/png"); - } - - [HttpPost("update")] - [Authorize] - public async Task Update([FromForm] AccountData data) - { - User user = await _userManager.GetUserAsync(HttpContext.User); - - if (!string.IsNullOrEmpty(data.Email)) - user.Email = data.Email; - if (!string.IsNullOrEmpty(data.Username)) - user.UserName = data.Username; - if (data.Picture?.Length > 0) - { - string path = Path.Combine(_picturePath, user.Id); - await using FileStream file = System.IO.File.Create(path); - await data.Picture.CopyToAsync(file); - } - await _userManager.UpdateAsync(user); - return Ok(); - } - - [HttpGet("default-permissions")] - public ActionResult> GetDefaultPermissions() - { - return _configuration.GetValue("defaultPermissions").Split(","); - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.IO; +// using System.Linq; +// using System.Security.Claims; +// using System.Threading.Tasks; +// using IdentityServer4.Models; +// using IdentityServer4.Services; +// using Kyoo.Authentication.Models.DTO; +// using Kyoo.Models; +// using Microsoft.AspNetCore.Authorization; +// using Microsoft.AspNetCore.Http; +// using Microsoft.AspNetCore.Identity; +// using Microsoft.AspNetCore.Mvc; +// using Microsoft.Extensions.Configuration; +// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; +// +// namespace Kyoo.Authentication.Views +// { +// public class LoginRequest +// { +// public string Username { get; set; } +// public string Password { get; set; } +// public bool StayLoggedIn { get; set; } +// } +// +// public class OtacRequest +// { +// public string Otac { get; set; } +// public bool StayLoggedIn { get; set; } +// } +// +// public class AccountData +// { +// [FromForm(Name = "email")] +// public string Email { get; set; } +// [FromForm(Name = "username")] +// public string Username { get; set; } +// [FromForm(Name = "picture")] +// public IFormFile Picture { get; set; } +// } +// +// +// /// +// /// The class responsible for login, logout, permissions and claims of a user. +// /// +// [Route("api/account")] +// [Route("api/accounts")] +// [ApiController] +// public class AccountApi : Controller, IProfileService +// { +// private readonly UserManager _userManager; +// private readonly SignInManager _signInManager; +// private readonly IConfiguration _configuration; +// private readonly string _picturePath; +// +// // TODO find how SignInManager & UserManager are implement and check if they can be used or not. +// public AccountApi(UserManager userManager, +// SignInManager signInManager, +// IConfiguration configuration) +// { +// _userManager = userManager; +// _signInManager = signInManager; +// _picturePath = configuration.GetValue("profilePicturePath"); +// _configuration = configuration; +// if (!Path.IsPathRooted(_picturePath)) +// _picturePath = Path.GetFullPath(_picturePath); +// } +// +// [HttpPost("register")] +// public async Task Register([FromBody] RegisterRequest request) +// { +// User user = request.ToUser(); +// IdentityResult result = await _userManager.CreateAsync(user, user.Password); +// if (!result.Succeeded) +// return BadRequest(result.Errors); +// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1)); +// await _userManager.UpdateAsync(account); +// await _userManager.AddClaimAsync(account, new Claim( +// "permissions", +// _configuration.GetValue("newUserPermissions"))); +// return Ok(new {otac}); +// } +// +// [HttpPost("login")] +// public async Task Login([FromBody] LoginRequest login) +// { +// if (!ModelState.IsValid) +// return BadRequest(login); +// SignInResult result = await _signInManager +// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false); +// if (result.Succeeded) +// return Ok(); +// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}}); +// } +// +// [HttpPost("otac-login")] +// public async Task OtacLogin([FromBody] OtacRequest otac) +// { +// if (!ModelState.IsValid) +// return BadRequest(otac); +// User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac); +// if (user == null) +// return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}}); +// if (user.OTACExpires <= DateTime.UtcNow) +// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}}); +// await _signInManager.SignInAsync(user, otac.StayLoggedIn); +// return Ok(); +// } +// +// [HttpGet("logout")] +// [Authorize] +// public async Task Logout() +// { +// await _signInManager.SignOutAsync(); +// return Ok(); +// } +// +// public async Task GetProfileDataAsync(ProfileDataRequestContext context) +// { +// User user = await _userManager.GetUserAsync(context.Subject); +// if (user != null) +// { +// List claims = new() +// { +// new Claim("email", user.Email), +// new Claim("username", user.UserName), +// new Claim("picture", $"api/account/picture/{user.UserName}") +// }; +// +// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); +// if (perms != null) +// claims.Add(perms); +// +// context.IssuedClaims.AddRange(claims); +// } +// } +// +// public async Task IsActiveAsync(IsActiveContext context) +// { +// User user = await _userManager.GetUserAsync(context.Subject); +// context.IsActive = user != null; +// } +// +// [HttpGet("picture/{username}")] +// public async Task GetPicture(string username) +// { +// User user = await _userManager.FindByNameAsync(username); +// if (user == null) +// return BadRequest(); +// string path = Path.Combine(_picturePath, user.Id); +// if (!System.IO.File.Exists(path)) +// return NotFound(); +// return new PhysicalFileResult(path, "image/png"); +// } +// +// [HttpPost("update")] +// [Authorize] +// public async Task Update([FromForm] AccountData data) +// { +// User user = await _userManager.GetUserAsync(HttpContext.User); +// +// if (!string.IsNullOrEmpty(data.Email)) +// user.Email = data.Email; +// if (!string.IsNullOrEmpty(data.Username)) +// user.UserName = data.Username; +// if (data.Picture?.Length > 0) +// { +// string path = Path.Combine(_picturePath, user.Id); +// await using FileStream file = System.IO.File.Create(path); +// await data.Picture.CopyToAsync(file); +// } +// await _userManager.UpdateAsync(user); +// return Ok(); +// } +// +// [HttpGet("default-permissions")] +// public ActionResult> GetDefaultPermissions() +// { +// return _configuration.GetValue("defaultPermissions").Split(","); +// } +// } +// } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs index 297f3b1d..c4c4231b 100644 --- a/Kyoo.Common/Models/Resources/IResource.cs +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -1,30 +1,23 @@ -using System; -using System.Collections.Generic; - namespace Kyoo.Models { + /// + /// An interface to represent a resource that can be retrieved from the database. + /// public interface IResource { + /// + /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. + /// public int ID { get; set; } + + /// + /// A human-readable identifier that can be used instead of an ID. + /// A slug must be unique for a type of resource but it can be changed. + /// + /// + /// There is no setter for a slug since it can be computed from other fields. + /// For example, a season slug is {ShowSlug}-s{SeasonNumber}. + /// public string Slug { get; } } - - public class ResourceComparer : IEqualityComparer where T : IResource - { - public bool Equals(T x, T y) - { - if (ReferenceEquals(x, y)) - return true; - if (ReferenceEquals(x, null)) - return false; - if (ReferenceEquals(y, null)) - return false; - return x.ID == y.ID || x.Slug == y.Slug; - } - - public int GetHashCode(T obj) - { - return HashCode.Combine(obj.ID, obj.Slug); - } - } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs new file mode 100644 index 00000000..a4367c11 --- /dev/null +++ b/Kyoo.Common/Models/Resources/User.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Kyoo.Models +{ + /// + /// A single user of the app. + /// + public class User : IResource + { + /// + public int ID { get; set; } + + /// + public string Slug { get; set; } + + /// + /// A username displayed to the user. + /// + public string Username { get; set; } + + /// + /// The user email address. + /// + public string Email { get; set; } + + /// + /// The user password (hashed, it can't be read like that). The hashing format is implementation defined. + /// + public string Password { get; set; } + + /// + /// The list of shows the user has finished. + /// + public ICollection Watched { get; set; } + + /// + /// The list of episodes the user is watching (stopped in progress or the next episode of the show) + /// + public ICollection<(Episode episode, int watchedPercentage)> CurrentlyWatching { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index bbbe177b..ff44761a 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -712,72 +712,6 @@ namespace Kyoo }, TaskContinuationOptions.ExecuteSynchronously); } - public static Expression> ResourceEquals(IResource obj) - where T : IResource - { - if (obj.ID > 0) - return x => x.ID == obj.ID || x.Slug == obj.Slug; - return x => x.Slug == obj.Slug; - } - - public static Func ResourceEqualsFunc(IResource obj) - where T : IResource - { - if (obj.ID > 0) - return x => x.ID == obj.ID || x.Slug == obj.Slug; - return x => x.Slug == obj.Slug; - } - - public static bool ResourceEquals([CanBeNull] object first, [CanBeNull] object second) - { - if (ReferenceEquals(first, second)) - return true; - if (first is IResource f && second is IResource s) - return ResourceEquals(f, s); - IEnumerable eno = first as IEnumerable; - IEnumerable ens = second as IEnumerable; - if (eno == null || ens == null) - throw new ArgumentException("Arguments are not resources or lists of resources."); - Type type = GetEnumerableType(eno); - if (typeof(IResource).IsAssignableFrom(type)) - return ResourceEquals(eno.Cast(), ens.Cast()); - return RunGenericMethod(typeof(Enumerable), "SequenceEqual", type, first, second); - } - - public static bool ResourceEquals([CanBeNull] T first, [CanBeNull] T second) - where T : IResource - { - if (ReferenceEquals(first, second)) - return true; - if (first == null || second == null) - return false; - return first.ID == second.ID || first.Slug == second.Slug; - } - - public static bool ResourceEquals([CanBeNull] IEnumerable first, [CanBeNull] IEnumerable second) - where T : IResource - { - if (ReferenceEquals(first, second)) - return true; - if (first == null || second == null) - return false; - return first.SequenceEqual(second, new ResourceComparer()); - } - - public static bool LinkEquals([CanBeNull] T first, int? firstID, [CanBeNull] T second, int? secondID) - where T : IResource - { - if (ResourceEquals(first, second)) - return true; - if (first == null && second != null - && firstID == second.ID) - return true; - if (first != null && second == null - && first.ID == secondID) - return true; - return firstID == secondID; - } - /// /// Get a friendly type name (supporting generics) /// For example a list of string will be displayed as List<string> and not as List`1. diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index d92c208f..a795723e 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -64,6 +64,10 @@ namespace Kyoo /// All metadataIDs (ExternalIDs) of Kyoo. See . /// public DbSet MetadataIds { get; set; } + /// + /// The list of registered users. + /// + // public DbSet Users { get; set; } /// /// All people's role. See . diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index c172fd67..33eb9a67 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -124,6 +124,8 @@ namespace Kyoo return next(); }); app.UseResponseCompression(); + + _plugins.ConfigureAspnet(app); app.UseSpa(spa => { @@ -133,8 +135,6 @@ namespace Kyoo spa.UseAngularCliServer("start"); }); - _plugins.ConfigureAspnet(app); - app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 246b84de..b521f8a8 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -33,7 +33,8 @@ "permissions": { "default": ["read", "play", "write", "admin"], "newUser": ["read", "play", "write", "admin"] - } + }, + "profilePicturePath": "users/" }, @@ -47,7 +48,6 @@ "transcodeTempPath": "cached/kyoo/transcode", "peoplePath": "people", "providerPath": "providers", - "profilePicturePath": "users/", "plugins": "plugins/", "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", "subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" From d9cca97961207ca57a5e8665727af6fcb1cb02dd Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 7 May 2021 01:45:26 +0200 Subject: [PATCH 16/25] Reworking login requests --- .../Controllers/PasswordUtils.cs | 54 +++ Kyoo.Authentication/Extensions.cs | 26 ++ .../Kyoo.Authentication.csproj | 1 + .../Models/DTO/LoginRequest.cs | 28 ++ Kyoo.Authentication/Models/DTO/OtacRequest.cs | 18 + .../Models/Options/AuthenticationOptions.cs | 28 ++ .../Models/{ => Options}/CertificateOption.cs | 0 .../Models/{ => Options}/PermissionOption.cs | 4 +- Kyoo.Authentication/Views/AccountApi.cs | 401 ++++++++++-------- Kyoo.Common/Controllers/IRepository.cs | 61 +-- Kyoo.Common/Models/Resources/User.cs | 24 +- Kyoo.Common/Models/WatchedEpisode.cs | 30 ++ Kyoo.Common/Utility.cs | 2 +- Kyoo.CommonAPI/DatabaseContext.cs | 4 +- Kyoo.CommonAPI/LocalRepository.cs | 6 +- Kyoo/Controllers/ProviderManager.cs | 2 +- .../Repositories/UserRepository.cs | 55 +++ 17 files changed, 523 insertions(+), 221 deletions(-) create mode 100644 Kyoo.Authentication/Controllers/PasswordUtils.cs create mode 100644 Kyoo.Authentication/Extensions.cs create mode 100644 Kyoo.Authentication/Models/DTO/LoginRequest.cs create mode 100644 Kyoo.Authentication/Models/DTO/OtacRequest.cs create mode 100644 Kyoo.Authentication/Models/Options/AuthenticationOptions.cs rename Kyoo.Authentication/Models/{ => Options}/CertificateOption.cs (100%) rename Kyoo.Authentication/Models/{ => Options}/PermissionOption.cs (83%) create mode 100644 Kyoo.Common/Models/WatchedEpisode.cs create mode 100644 Kyoo/Controllers/Repositories/UserRepository.cs diff --git a/Kyoo.Authentication/Controllers/PasswordUtils.cs b/Kyoo.Authentication/Controllers/PasswordUtils.cs new file mode 100644 index 00000000..d28aaa99 --- /dev/null +++ b/Kyoo.Authentication/Controllers/PasswordUtils.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using IdentityModel; + +namespace Kyoo.Authentication +{ + public static class PasswordUtils + { + /// + /// Generate an OneTimeAccessCode. + /// + /// A new otac. + public static string GenerateOTAC() + { + return CryptoRandom.CreateUniqueId(); + } + + /// + /// Hash a password to store it has a verification only. + /// + /// The password to hash + /// The hashed password + public static string HashPassword(string password) + { + byte[] salt = new byte[16]; + new RNGCryptoServiceProvider().GetBytes(salt); + Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000); + byte[] hash = pbkdf2.GetBytes(20); + byte[] hashBytes = new byte[36]; + Array.Copy(salt, 0, hashBytes, 0, 16); + Array.Copy(hash, 0, hashBytes, 16, 20); + return Convert.ToBase64String(hashBytes); + } + + /// + /// Check if a password is the same as a valid hashed password. + /// + /// The password to check + /// + /// The valid hashed password. This password must be hashed via . + /// + /// True if the password is valid, false otherwise. + public static bool CheckPassword(string password, string validPassword) + { + byte[] validHash = Convert.FromBase64String(validPassword); + byte[] salt = new byte[16]; + Array.Copy(validHash, 0, salt, 0, 16); + Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000); + byte[] hash = pbkdf2.GetBytes(20); + return hash.SequenceEqual(validHash.Skip(16)); + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs new file mode 100644 index 00000000..b844ce5f --- /dev/null +++ b/Kyoo.Authentication/Extensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Security.Claims; +using IdentityModel; +using Kyoo.Models; + +namespace Kyoo.Authentication +{ + /// + /// Extension methods. + /// + public static class Extensions + { + public static ClaimsPrincipal ToPrincipal(this User user) + { + List claims = new() + { + new Claim(JwtClaimTypes.Subject, user.ID.ToString()), + new Claim(JwtClaimTypes.Name, user.Username), + new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") + }; + + ClaimsIdentity id = new (claims); + return new ClaimsPrincipal(id); + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj index 7bb0a7fb..b334a943 100644 --- a/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -19,6 +19,7 @@ + diff --git a/Kyoo.Authentication/Models/DTO/LoginRequest.cs b/Kyoo.Authentication/Models/DTO/LoginRequest.cs new file mode 100644 index 00000000..9bee4e04 --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/LoginRequest.cs @@ -0,0 +1,28 @@ +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A model only used on login requests. + /// + public class LoginRequest + { + /// + /// The user's username. + /// + public string Username { get; set; } + + /// + /// The user's password. + /// + public string Password { get; set; } + + /// + /// Should the user stay logged in? If true a cookie will be put. + /// + public bool StayLoggedIn { get; set; } + + /// + /// The return url of the login flow. + /// + public string ReturnURL { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Models/DTO/OtacRequest.cs b/Kyoo.Authentication/Models/DTO/OtacRequest.cs new file mode 100644 index 00000000..0c007f78 --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/OtacRequest.cs @@ -0,0 +1,18 @@ +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A model to represent an otac request + /// + public class OtacRequest + { + /// + /// The One Time Access Code + /// + public string Otac { get; set; } + + /// + /// Should the user stay logged + /// + public bool StayLoggedIn { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs b/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs new file mode 100644 index 00000000..f35e22a3 --- /dev/null +++ b/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs @@ -0,0 +1,28 @@ +namespace Kyoo.Authentication.Models +{ + /// + /// The main authentication options. + /// + public class AuthenticationOptions + { + /// + /// The path to get this option from the root configuration. + /// + public const string Path = "authentication"; + + /// + /// The options for certificates + /// + public CertificateOption Certificate { get; set; } + + /// + /// Options for permissions + /// + public PermissionOption Permissions { get; set; } + + /// + /// Root path of user's profile pictures. + /// + public string ProfilePicturePath { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Models/CertificateOption.cs b/Kyoo.Authentication/Models/Options/CertificateOption.cs similarity index 100% rename from Kyoo.Authentication/Models/CertificateOption.cs rename to Kyoo.Authentication/Models/Options/CertificateOption.cs diff --git a/Kyoo.Authentication/Models/PermissionOption.cs b/Kyoo.Authentication/Models/Options/PermissionOption.cs similarity index 83% rename from Kyoo.Authentication/Models/PermissionOption.cs rename to Kyoo.Authentication/Models/Options/PermissionOption.cs index 772f3fb5..0a26f89e 100644 --- a/Kyoo.Authentication/Models/PermissionOption.cs +++ b/Kyoo.Authentication/Models/Options/PermissionOption.cs @@ -15,11 +15,11 @@ namespace Kyoo.Authentication.Models /// /// The default permissions that will be given to a non-connected user. /// - public ICollection Default { get; set; } + public string[] Default { get; set; } /// /// Permissions applied to a new user. /// - public ICollection NewUser { get; set; } + public string[] NewUser { get; set; } } } \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index de47d57d..94cae0ad 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -1,183 +1,218 @@ -// using System; -// using System.Collections.Generic; -// using System.IO; -// using System.Linq; -// using System.Security.Claims; -// using System.Threading.Tasks; -// using IdentityServer4.Models; -// using IdentityServer4.Services; -// using Kyoo.Authentication.Models.DTO; -// using Kyoo.Models; -// using Microsoft.AspNetCore.Authorization; -// using Microsoft.AspNetCore.Http; -// using Microsoft.AspNetCore.Identity; -// using Microsoft.AspNetCore.Mvc; -// using Microsoft.Extensions.Configuration; -// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; -// -// namespace Kyoo.Authentication.Views -// { -// public class LoginRequest -// { -// public string Username { get; set; } -// public string Password { get; set; } -// public bool StayLoggedIn { get; set; } -// } -// -// public class OtacRequest -// { -// public string Otac { get; set; } -// public bool StayLoggedIn { get; set; } -// } -// -// public class AccountData -// { -// [FromForm(Name = "email")] -// public string Email { get; set; } -// [FromForm(Name = "username")] -// public string Username { get; set; } -// [FromForm(Name = "picture")] -// public IFormFile Picture { get; set; } -// } -// -// -// /// -// /// The class responsible for login, logout, permissions and claims of a user. -// /// -// [Route("api/account")] -// [Route("api/accounts")] -// [ApiController] -// public class AccountApi : Controller, IProfileService -// { -// private readonly UserManager _userManager; -// private readonly SignInManager _signInManager; -// private readonly IConfiguration _configuration; -// private readonly string _picturePath; -// -// // TODO find how SignInManager & UserManager are implement and check if they can be used or not. -// public AccountApi(UserManager userManager, -// SignInManager signInManager, -// IConfiguration configuration) -// { -// _userManager = userManager; -// _signInManager = signInManager; -// _picturePath = configuration.GetValue("profilePicturePath"); -// _configuration = configuration; -// if (!Path.IsPathRooted(_picturePath)) -// _picturePath = Path.GetFullPath(_picturePath); -// } -// -// [HttpPost("register")] -// public async Task Register([FromBody] RegisterRequest request) -// { -// User user = request.ToUser(); -// IdentityResult result = await _userManager.CreateAsync(user, user.Password); -// if (!result.Succeeded) -// return BadRequest(result.Errors); -// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1)); -// await _userManager.UpdateAsync(account); -// await _userManager.AddClaimAsync(account, new Claim( -// "permissions", -// _configuration.GetValue("newUserPermissions"))); -// return Ok(new {otac}); -// } -// -// [HttpPost("login")] -// public async Task Login([FromBody] LoginRequest login) -// { -// if (!ModelState.IsValid) -// return BadRequest(login); -// SignInResult result = await _signInManager -// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false); -// if (result.Succeeded) -// return Ok(); -// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}}); -// } -// -// [HttpPost("otac-login")] -// public async Task OtacLogin([FromBody] OtacRequest otac) -// { -// if (!ModelState.IsValid) -// return BadRequest(otac); -// User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac); -// if (user == null) -// return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}}); -// if (user.OTACExpires <= DateTime.UtcNow) -// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}}); -// await _signInManager.SignInAsync(user, otac.StayLoggedIn); -// return Ok(); -// } -// -// [HttpGet("logout")] -// [Authorize] -// public async Task Logout() -// { -// await _signInManager.SignOutAsync(); -// return Ok(); -// } -// -// public async Task GetProfileDataAsync(ProfileDataRequestContext context) -// { -// User user = await _userManager.GetUserAsync(context.Subject); -// if (user != null) -// { -// List claims = new() -// { -// new Claim("email", user.Email), -// new Claim("username", user.UserName), -// new Claim("picture", $"api/account/picture/{user.UserName}") -// }; -// -// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); -// if (perms != null) -// claims.Add(perms); -// -// context.IssuedClaims.AddRange(claims); -// } -// } -// -// public async Task IsActiveAsync(IsActiveContext context) -// { -// User user = await _userManager.GetUserAsync(context.Subject); -// context.IsActive = user != null; -// } -// -// [HttpGet("picture/{username}")] -// public async Task GetPicture(string username) -// { -// User user = await _userManager.FindByNameAsync(username); -// if (user == null) -// return BadRequest(); -// string path = Path.Combine(_picturePath, user.Id); -// if (!System.IO.File.Exists(path)) -// return NotFound(); -// return new PhysicalFileResult(path, "image/png"); -// } -// -// [HttpPost("update")] -// [Authorize] -// public async Task Update([FromForm] AccountData data) -// { -// User user = await _userManager.GetUserAsync(HttpContext.User); -// -// if (!string.IsNullOrEmpty(data.Email)) -// user.Email = data.Email; -// if (!string.IsNullOrEmpty(data.Username)) -// user.UserName = data.Username; -// if (data.Picture?.Length > 0) -// { -// string path = Path.Combine(_picturePath, user.Id); -// await using FileStream file = System.IO.File.Create(path); -// await data.Picture.CopyToAsync(file); -// } -// await _userManager.UpdateAsync(user); -// return Ok(); -// } -// -// [HttpGet("default-permissions")] -// public ActionResult> GetDefaultPermissions() -// { -// return _configuration.GetValue("defaultPermissions").Split(","); -// } -// } -// } \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityModel; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Kyoo.Authentication.Models.DTO; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions; + +namespace Kyoo.Authentication.Views +{ + public class AccountData + { + [FromForm(Name = "email")] + public string Email { get; set; } + [FromForm(Name = "username")] + public string Username { get; set; } + [FromForm(Name = "picture")] + public IFormFile Picture { get; set; } + } + + + /// + /// The class responsible for login, logout, permissions and claims of a user. + /// + [Route("api/account")] + [Route("api/accounts")] + [ApiController] + public class AccountApi : Controller, IProfileService + { + /// + /// The repository to handle users. + /// + private readonly IUserRepository _users; + /// + /// The identity server interaction service to login users. + /// + private readonly IIdentityServerInteractionService _interaction; + /// + /// Options about authentication. Those options are monitored and reloads are supported. + /// + private readonly IOptionsMonitor _options; + + + /// + /// Create a new handle to handle login/users requests. + /// + /// The user repository to create and manage users + /// The identity server interaction service to login users. + /// Authentication options (this may be hot reloaded) + public AccountApi(IUserRepository users, + IIdentityServerInteractionService interaction, + IOptionsMonitor options) + { + _users = users; + _interaction = interaction; + _options = options; + } + + + /// + /// Register a new user and return a OTAC to connect to it. + /// + /// The DTO register request + /// A OTAC to connect to this new account + [HttpPost("register")] + public async Task> Register([FromBody] RegisterRequest request) + { + User user = request.ToUser(); + user.Permissions = _options.CurrentValue.Permissions.NewUser; + user.Password = PasswordUtils.HashPassword(user.Password); + user.ExtraData["otac"] = PasswordUtils.GenerateOTAC(); + user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s"); + await _users.Create(user); + return user.ExtraData["otac"]; + } + + /// + /// Return an authentication properties based on a stay login property + /// + /// Should the user stay logged + /// Authentication properties based on a stay login + private static AuthenticationProperties StayLogged(bool stayLogged) + { + if (!stayLogged) + return null; + return new AuthenticationProperties + { + IsPersistent = true, + ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1) + }; + } + + /// + /// Login the user. + /// + /// The DTO login request + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest login) + { + AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); + User user = await _users.Get(x => x.Username == login.Username); + + if (context == null || user == null) + return Unauthorized(); + if (!PasswordUtils.CheckPassword(login.Password, user.Password)) + return Unauthorized(); + + await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(login.StayLoggedIn)); + return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true }); + } + + /// + /// Use a OTAC to login a user. + /// + /// The OTAC request + [HttpPost("otac-login")] + public async Task OtacLogin([FromBody] OtacRequest otac) + { + User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac); + if (user == null) + return Unauthorized(); + if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow) + return BadRequest(new + { + code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password." + }); + await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(otac.StayLoggedIn)); + return Ok(); + } + + /// + /// Sign out an user + /// + [HttpGet("logout")] + [Authorize] + public async Task Logout() + { + await HttpContext.SignOutAsync(); + return Ok(); + } + + // TODO check with the extension method + public async Task GetProfileDataAsync(ProfileDataRequestContext context) + { + User user = await _userManager.GetUserAsync(context.Subject); + if (user != null) + { + List claims = new() + { + new Claim(JwtClaimTypes.Email, user.Email), + new Claim(JwtClaimTypes.Name, user.Username), + new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") + }; + + Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); + if (perms != null) + claims.Add(perms); + + context.IssuedClaims.AddRange(claims); + } + } + + public async Task IsActiveAsync(IsActiveContext context) + { + User user = await _userManager.GetUserAsync(context.Subject); + context.IsActive = user != null; + } + + [HttpGet("picture/{username}")] + public async Task GetPicture(string username) + { + User user = await _userManager.FindByNameAsync(username); + if (user == null) + return BadRequest(); + string path = Path.Combine(_picturePath, user.Id); + if (!System.IO.File.Exists(path)) + return NotFound(); + return new PhysicalFileResult(path, "image/png"); + } + + [HttpPost("update")] + [Authorize] + public async Task Update([FromForm] AccountData data) + { + User user = await _userManager.GetUserAsync(HttpContext.User); + + if (!string.IsNullOrEmpty(data.Email)) + user.Email = data.Email; + if (!string.IsNullOrEmpty(data.Username)) + user.UserName = data.Username; + if (data.Picture?.Length > 0) + { + string path = Path.Combine(_picturePath, user.Id); + await using FileStream file = System.IO.File.Create(path); + await data.Picture.CopyToAsync(file); + } + await _userManager.UpdateAsync(user); + return Ok(); + } + + [HttpGet("default-permissions")] + public ActionResult> GetDefaultPermissions() + { + return _configuration.GetValue("defaultPermissions").Split(","); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index bc5ed0bc..4e8b16a3 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -11,7 +11,7 @@ using Kyoo.Models.Exceptions; namespace Kyoo.Controllers { /// - /// Informations about the pagination. How many items should be displayed and where to start. + /// Information about the pagination. How many items should be displayed and where to start. /// public readonly struct Pagination { @@ -44,7 +44,7 @@ namespace Kyoo.Controllers } /// - /// Informations about how a query should be sorted. What factor should decide the sort and in which order. + /// Information about how a query should be sorted. What factor should decide the sort and in which order. /// /// For witch type this sort applies public readonly struct Sort @@ -54,7 +54,7 @@ namespace Kyoo.Controllers /// public Expression> Key { get; } /// - /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendent order. + /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order. /// public bool Descendant { get; } @@ -175,7 +175,7 @@ namespace Kyoo.Controllers /// Get every resources that match all filters /// /// A filter predicate - /// Sort informations about the query (sort by, sort order) + /// Sort information about the query (sort by, sort order) /// How pagination should be done (where to start and how many to return) /// A list of resources that match every filters Task> GetAll(Expression> where = null, @@ -205,24 +205,24 @@ namespace Kyoo.Controllers /// Create a new resource. /// /// The item to register - /// The resource registers and completed by database's informations (related items & so on) + /// The resource registers and completed by database's information (related items & so on) Task Create([NotNull] T obj); /// /// Create a new resource if it does not exist already. If it does, the existing value is returned instead. /// /// The object to create - /// Allow issues to occurs in this method. Every issue is catched and ignored. + /// Allow issues to occurs in this method. Every issue is caught and ignored. /// The newly created item or the existing value if it existed. Task CreateIfNotExists([NotNull] T obj, bool silentFail = false); /// /// Edit a resource /// - /// The resourcce to edit, it's ID can't change. + /// The resource to edit, it's ID can't change. /// Should old properties of the resource be discarded or should null values considered as not changed? /// If the item is not found - /// The resource edited and completed by database's informations (related items & so on) + /// The resource edited and completed by database's information (related items & so on) Task Edit([NotNull] T edited, bool resetOld); /// @@ -259,25 +259,25 @@ namespace Kyoo.Controllers /// /// Delete a list of resources. /// - /// One or multiple resources's id + /// One or multiple resource's id /// If the item is not found Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); /// /// Delete a list of resources. /// - /// An enumearble of resources's id + /// An enumerable of resource's id /// If the item is not found Task DeleteRange(IEnumerable ids); /// /// Delete a list of resources. /// - /// One or multiple resources's slug + /// One or multiple resource's slug /// If the item is not found Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); /// /// Delete a list of resources. /// - /// An enumerable of resources's slug + /// An enumerable of resource's slug /// If the item is not found Task DeleteRange(IEnumerable slugs); /// @@ -294,7 +294,7 @@ namespace Kyoo.Controllers public interface IShowRepository : IRepository { /// - /// Link a show to a collection and/or a library. The given show is now part of thoses containers. + /// Link a show to a collection and/or a library. The given show is now part of those containers. /// If both a library and a collection are given, the collection is added to the library too. /// /// The ID of the show @@ -421,7 +421,7 @@ namespace Kyoo.Controllers /// The slug of the track /// The type (Video, Audio or Subtitle) /// If the item is not found - /// The tracl found + /// The track found Task Get(string slug, StreamType type = StreamType.Unknown); /// @@ -429,7 +429,7 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// The tracl found + /// The track found Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); } @@ -439,16 +439,16 @@ namespace Kyoo.Controllers public interface ILibraryRepository : IRepository { } /// - /// A repository to handle library items (A wrapper arround shows and collections). + /// A repository to handle library items (A wrapper around shows and collections). /// public interface ILibraryItemRepository : IRepository { /// - /// Get items (A wrapper arround shows or collections) from a library. + /// Get items (A wrapper around shows or collections) from a library. /// /// The ID of the library /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters public Task> GetFromLibrary(int id, @@ -456,7 +456,7 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default); /// - /// Get items (A wrapper arround shows or collections) from a library. + /// Get items (A wrapper around shows or collections) from a library. /// /// The ID of the library /// A filter function @@ -470,11 +470,11 @@ namespace Kyoo.Controllers ) => GetFromLibrary(id, where, new Sort(sort), limit); /// - /// Get items (A wrapper arround shows or collections) from a library. + /// Get items (A wrapper around shows or collections) from a library. /// /// The slug of the library /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters public Task> GetFromLibrary(string slug, @@ -482,7 +482,7 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default); /// - /// Get items (A wrapper arround shows or collections) from a library. + /// Get items (A wrapper around shows or collections) from a library. /// /// The slug of the library /// A filter function @@ -521,7 +521,7 @@ namespace Kyoo.Controllers /// /// The ID of the show /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters Task> GetFromShow(int showID, @@ -547,7 +547,7 @@ namespace Kyoo.Controllers /// /// The slug of the show /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters Task> GetFromShow(string showSlug, @@ -573,7 +573,7 @@ namespace Kyoo.Controllers /// /// The id of the person /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters Task> GetFromPeople(int id, @@ -599,7 +599,7 @@ namespace Kyoo.Controllers /// /// The slug of the person /// A filter function - /// Sort informations (sort order & sort by) + /// Sort information (sort order & sort by) /// How many items to return and where to start /// A list of items that match every filters Task> GetFromPeople(string slug, @@ -631,7 +631,7 @@ namespace Kyoo.Controllers /// /// A predicate to add arbitrary filter /// Sort information (sort order & sort by) - /// Paginations information (where to start and how many to get) + /// Pagination information (where to start and how many to get) /// A filtered list of external ids. Task> GetMetadataID(Expression> where = null, Sort sort = default, @@ -642,11 +642,16 @@ namespace Kyoo.Controllers /// /// A predicate to add arbitrary filter /// A sort by expression - /// Paginations information (where to start and how many to get) + /// Pagination information (where to start and how many to get) /// A filtered list of external ids. Task> GetMetadataID([Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetMetadataID(where, new Sort(sort), limit); } + + /// + /// A repository to handle users. + /// + public interface IUserRepository : IRepository {} } diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index a4367c11..da85af0b 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -28,6 +28,16 @@ namespace Kyoo.Models /// public string Password { get; set; } + /// + /// The list of permissions of the user. The format of this is implementation dependent. + /// + public string[] Permissions { get; set; } + + /// + /// Arbitrary extra data that can be used by specific authentication implementations. + /// + public Dictionary ExtraData { get; set; } + /// /// The list of shows the user has finished. /// @@ -36,6 +46,18 @@ namespace Kyoo.Models /// /// The list of episodes the user is watching (stopped in progress or the next episode of the show) /// - public ICollection<(Episode episode, int watchedPercentage)> CurrentlyWatching { get; set; } + public ICollection CurrentlyWatching { get; set; } + +#if ENABLE_INTERNAL_LINKS + /// + /// Links between Users and Shows. + /// + public ICollection> ShowLinks { get; set; } + + /// + /// Links between Users and WatchedEpisodes. + /// + public ICollection> EpisodeLinks { get; set; } +#endif } } \ No newline at end of file diff --git a/Kyoo.Common/Models/WatchedEpisode.cs b/Kyoo.Common/Models/WatchedEpisode.cs new file mode 100644 index 00000000..631c7572 --- /dev/null +++ b/Kyoo.Common/Models/WatchedEpisode.cs @@ -0,0 +1,30 @@ +using Kyoo.Models.Attributes; + +namespace Kyoo.Models +{ + /// + /// Metadata of episode currently watching by an user + /// + public class WatchedEpisode : IResource + { + /// + [SerializeIgnore] public int ID + { + get => Episode.ID; + set => Episode.ID = value; + } + + /// + [SerializeIgnore] public string Slug => Episode.Slug; + + /// + /// The episode currently watched + /// + public Episode Episode { get; set; } + + /// + /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). + /// + public int WatchedPercentage { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index ff44761a..6583f704 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -215,7 +215,7 @@ namespace Kyoo /// /// An advanced function. /// This will set missing values of to the corresponding values of . - /// Enumerables will be merged (concatened). + /// Enumerable will be merged (concatenated). /// At the end, the OnMerge method of first will be called if first is a . /// /// The object to complete diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index a795723e..4a4b416b 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -67,7 +67,7 @@ namespace Kyoo /// /// The list of registered users. /// - // public DbSet Users { get; set; } + public DbSet Users { get; set; } /// /// All people's role. See . @@ -254,7 +254,7 @@ namespace Kyoo /// Return a new or an in cache temporary object wih the same ID as the one given /// /// If a resource with the same ID is found in the database, it will be used. - /// will be used overwise + /// will be used otherwise /// The type of the resource /// A resource that is now tracked by this context. public T GetTemporaryObject(T model) diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 32b47bd2..ee01b089 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -118,7 +118,7 @@ namespace Kyoo.Controllers /// The base query to filter. /// An expression to filter based on arbitrary conditions /// The sort settings (sort order & sort by) - /// Paginations information (where to start and how many to get) + /// Pagination information (where to start and how many to get) /// The filtered query protected Task> ApplyFilters(IQueryable query, Expression> where = null, @@ -137,7 +137,7 @@ namespace Kyoo.Controllers /// The base query to filter. /// An expression to filter based on arbitrary conditions /// The sort settings (sort order & sort by) - /// Paginations information (where to start and how many to get) + /// Pagination information (where to start and how many to get) /// The filtered query protected async Task> ApplyFilters(IQueryable query, Func> get, @@ -244,7 +244,7 @@ namespace Kyoo.Controllers } /// - /// An overridable method to edit relatiosn of a resource. + /// An overridable method to edit relation of a resource. /// /// The non edited resource /// The new version of . This item will be saved on the databse and replace diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index d025c31c..61f41593 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -33,7 +33,7 @@ namespace Kyoo.Controllers } catch (Exception ex) { await Console.Error.WriteLineAsync( - $"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}"); + $"The provider {provider.Provider.Name} could not work for {what}. Exception: {ex.Message}"); } } return ret; diff --git a/Kyoo/Controllers/Repositories/UserRepository.cs b/Kyoo/Controllers/Repositories/UserRepository.cs new file mode 100644 index 00000000..97bce050 --- /dev/null +++ b/Kyoo/Controllers/Repositories/UserRepository.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; + +namespace Kyoo.Controllers +{ + /// + /// A repository for users. + /// + public class UserRepository : LocalRepository, IUserRepository + { + /// + /// The database handle + /// + private readonly DatabaseContext _database; + + /// + protected override Expression> DefaultSort => x => x.Username; + + + /// + /// Create a new + /// + /// The database handle to use + public UserRepository(DatabaseContext database) + : base(database) + { + _database = database; + } + + /// + public override async Task> Search(string query) + { + return await _database.Users + .Where(_database.Like(x => x.Username, $"%{query}%")) + .OrderBy(DefaultSort) + .Take(20) + .ToListAsync(); + } + + /// + public override async Task Delete(User obj) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + _database.Entry(obj).State = EntityState.Deleted; + await _database.SaveChangesAsync(); + } + } +} \ No newline at end of file From 77231f4f417603f6fa633ad00a1e77da4252784e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 7 May 2021 23:13:33 +0200 Subject: [PATCH 17/25] Adding a new User class --- Kyoo.Authentication/AuthenticationModule.cs | 6 +- Kyoo.Authentication/Extensions.cs | 21 +++- .../Models/DTO/AccountUpdateRequest.cs | 28 +++++ Kyoo.Authentication/Views/AccountApi.cs | 78 +++++------- Kyoo.Common/Controllers/IFileManager.cs | 80 ++++++++++-- Kyoo.Common/Models/Resources/User.cs | 16 ++- Kyoo.Common/Models/WatchedEpisode.cs | 30 ----- Kyoo.CommonAPI/DatabaseContext.cs | 23 ++++ Kyoo.Postgresql/Kyoo.Postgresql.csproj | 13 +- ....cs => 20210507203809_Initial.Designer.cs} | 115 +++++++++++++++++- ...7_Initial.cs => 20210507203809_Initial.cs} | 93 ++++++++++++++ .../PostgresContextModelSnapshot.cs | 113 +++++++++++++++++ Kyoo.Postgresql/PostgresContext.cs | 4 + Kyoo.WebApp | 2 +- Kyoo/Controllers/FileManager.cs | 36 +++++- .../Repositories/ShowRepository.cs | 2 +- Kyoo/CoreModule.cs | 4 +- Kyoo/Startup.cs | 16 +-- Kyoo/Views/SubtitleApi.cs | 2 +- 19 files changed, 563 insertions(+), 119 deletions(-) create mode 100644 Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs delete mode 100644 Kyoo.Common/Models/WatchedEpisode.cs rename Kyoo.Postgresql/Migrations/{20210505182627_Initial.Designer.cs => 20210507203809_Initial.Designer.cs} (87%) rename Kyoo.Postgresql/Migrations/{20210505182627_Initial.cs => 20210507203809_Initial.cs} (86%) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 82d05cce..02a0f982 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -4,6 +4,7 @@ using IdentityServer4.Extensions; using IdentityServer4.Services; using Kyoo.Authentication.Models; using Kyoo.Controllers; +using Kyoo.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -36,7 +37,10 @@ namespace Kyoo.Authentication public ICollection ConditionalProvides => ArraySegment.Empty; /// - public ICollection Requires => ArraySegment.Empty; + public ICollection Requires => new [] + { + typeof(IUserRepository) + }; /// diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs index b844ce5f..4cac9dc8 100644 --- a/Kyoo.Authentication/Extensions.cs +++ b/Kyoo.Authentication/Extensions.cs @@ -10,16 +10,29 @@ namespace Kyoo.Authentication /// public static class Extensions { - public static ClaimsPrincipal ToPrincipal(this User user) + /// + /// Get claims of an user. + /// + /// The user concerned + /// The list of claims the user has + public static ICollection GetClaims(this User user) { - List claims = new() + return new[] { new Claim(JwtClaimTypes.Subject, user.ID.ToString()), new Claim(JwtClaimTypes.Name, user.Username), new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") }; - - ClaimsIdentity id = new (claims); + } + + /// + /// Convert a user to a ClaimsPrincipal. + /// + /// The user to convert + /// A ClaimsPrincipal representing the user + public static ClaimsPrincipal ToPrincipal(this User user) + { + ClaimsIdentity id = new (user.GetClaims()); return new ClaimsPrincipal(id); } } diff --git a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs new file mode 100644 index 00000000..4ec474d5 --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; + +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A model only used on account update requests. + /// + public class AccountUpdateRequest + { + /// + /// The new email address of the user + /// + [EmailAddress] + public string Email { get; set; } + + /// + /// The new username of the user. + /// + [MinLength(4)] + public string Username { get; set; } + + /// + /// The picture icon. + /// + public IFormFile Picture { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 94cae0ad..903c7e93 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -2,9 +2,8 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Security.Claims; using System.Threading.Tasks; -using IdentityModel; +using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; using Kyoo.Authentication.Models.DTO; @@ -12,24 +11,12 @@ using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions; namespace Kyoo.Authentication.Views { - public class AccountData - { - [FromForm(Name = "email")] - public string Email { get; set; } - [FromForm(Name = "username")] - public string Username { get; set; } - [FromForm(Name = "picture")] - public IFormFile Picture { get; set; } - } - - /// /// The class responsible for login, logout, permissions and claims of a user. /// @@ -46,6 +33,11 @@ namespace Kyoo.Authentication.Views /// The identity server interaction service to login users. /// private readonly IIdentityServerInteractionService _interaction; + /// + /// A file manager to send profile pictures + /// + private readonly IFileManager _files; + /// /// Options about authentication. Those options are monitored and reloads are supported. /// @@ -57,13 +49,16 @@ namespace Kyoo.Authentication.Views /// /// The user repository to create and manage users /// The identity server interaction service to login users. + /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, IIdentityServerInteractionService interaction, + IFileManager files, IOptionsMonitor options) { _users = users; _interaction = interaction; + _files = files; _options = options; } @@ -153,66 +148,51 @@ namespace Kyoo.Authentication.Views // TODO check with the extension method public async Task GetProfileDataAsync(ProfileDataRequestContext context) { - User user = await _userManager.GetUserAsync(context.Subject); - if (user != null) - { - List claims = new() - { - new Claim(JwtClaimTypes.Email, user.Email), - new Claim(JwtClaimTypes.Name, user.Username), - new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") - }; - - Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); - if (perms != null) - claims.Add(perms); - - context.IssuedClaims.AddRange(claims); - } + User user = await _users.Get(int.Parse(context.Subject.GetSubjectId())); + if (user == null) + return; + context.IssuedClaims.AddRange(user.GetClaims()); } public async Task IsActiveAsync(IsActiveContext context) { - User user = await _userManager.GetUserAsync(context.Subject); + User user = await _users.Get(int.Parse(context.Subject.GetSubjectId())); context.IsActive = user != null; } - [HttpGet("picture/{username}")] - public async Task GetPicture(string username) + [HttpGet("picture/{slug}")] + public async Task GetPicture(string slug) { - User user = await _userManager.FindByNameAsync(username); + User user = await _users.GetOrDefault(slug); if (user == null) - return BadRequest(); - string path = Path.Combine(_picturePath, user.Id); - if (!System.IO.File.Exists(path)) return NotFound(); - return new PhysicalFileResult(path, "image/png"); + string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); + return _files.FileResult(path); } - [HttpPost("update")] + [HttpPut] [Authorize] - public async Task Update([FromForm] AccountData data) + public async Task> Update([FromForm] AccountUpdateRequest data) { - User user = await _userManager.GetUserAsync(HttpContext.User); + User user = await _users.Get(int.Parse(HttpContext.User.GetSubjectId())); if (!string.IsNullOrEmpty(data.Email)) - user.Email = data.Email; + user.Email = data.Email; if (!string.IsNullOrEmpty(data.Username)) - user.UserName = data.Username; + user.Username = data.Username; if (data.Picture?.Length > 0) { - string path = Path.Combine(_picturePath, user.Id); - await using FileStream file = System.IO.File.Create(path); + string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); + await using Stream file = _files.NewFile(path); await data.Picture.CopyToAsync(file); } - await _userManager.UpdateAsync(user); - return Ok(); + return await _users.Edit(user, false); } - [HttpGet("default-permissions")] + [HttpGet("permissions")] public ActionResult> GetDefaultPermissions() { - return _configuration.GetValue("defaultPermissions").Split(","); + return _options.CurrentValue.Permissions.Default; } } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs index cc3c70bb..0765c1d0 100644 --- a/Kyoo.Common/Controllers/IFileManager.cs +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -7,21 +7,87 @@ using Microsoft.AspNetCore.Mvc; namespace Kyoo.Controllers { + /// + /// A service to abstract the file system to allow custom file systems (like distant file systems or external providers) + /// public interface IFileManager { - public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false); - - public StreamReader GetReader([NotNull] string path); - - public Task> ListFiles([NotNull] string path); - - public Task Exists([NotNull] string path); // TODO find a way to handle Transmux/Transcode with this system. + /// + /// Used for http queries returning a file. This should be used to return local files + /// or proxy them from a distant server + /// + /// + /// If no file exists at the given path, you should return a NotFoundResult or handle it gracefully. + /// + /// The path of the file. + /// + /// Should the file be downloaded at once or is the client allowed to request only part of the file + /// + /// + /// You can manually specify the content type of your file. + /// For example you can force a file to be returned as plain text using text/plain. + /// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file). + /// + /// An representing the file returned. + public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null); + + /// + /// Read a file present at . The reader can be used in an arbitrary context. + /// To return files from an http endpoint, use . + /// + /// The path of the file + /// If the file could not be found. + /// A reader to read the file. + public Stream GetReader([NotNull] string path); + + /// + /// Create a new file at . + /// + /// The path of the new file. + /// A writer to write to the new file. + public Stream NewFile([NotNull] string path); + + /// + /// List files in a directory. + /// + /// The path of the directory + /// A list of files's path. + public Task> ListFiles([NotNull] string path); + + /// + /// Check if a file exists at the given path. + /// + /// The path to check + /// True if the path exists, false otherwise + public Task Exists([NotNull] string path); + + /// + /// Get the extra directory of a show. + /// This method is in this system to allow a filesystem to use a different metadata policy for one. + /// It can be useful if the filesystem is readonly. + /// + /// The show to proceed + /// The extra directory of the show public string GetExtraDirectory(Show show); + /// + /// Get the extra directory of a season. + /// This method is in this system to allow a filesystem to use a different metadata policy for one. + /// It can be useful if the filesystem is readonly. + /// + /// The season to proceed + /// The extra directory of the season public string GetExtraDirectory(Season season); + /// + /// Get the extra directory of an episode. + /// This method is in this system to allow a filesystem to use a different metadata policy for one. + /// It can be useful if the filesystem is readonly. + /// + /// The episode to proceed + /// The extra directory of the episode public string GetExtraDirectory(Episode episode); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index da85af0b..94afa240 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -53,11 +53,17 @@ namespace Kyoo.Models /// Links between Users and Shows. /// public ICollection> ShowLinks { get; set; } - - /// - /// Links between Users and WatchedEpisodes. - /// - public ICollection> EpisodeLinks { get; set; } #endif } + + /// + /// Metadata of episode currently watching by an user + /// + public class WatchedEpisode : Link + { + /// + /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). + /// + public int WatchedPercentage { get; set; } + } } \ No newline at end of file diff --git a/Kyoo.Common/Models/WatchedEpisode.cs b/Kyoo.Common/Models/WatchedEpisode.cs deleted file mode 100644 index 631c7572..00000000 --- a/Kyoo.Common/Models/WatchedEpisode.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - /// - /// Metadata of episode currently watching by an user - /// - public class WatchedEpisode : IResource - { - /// - [SerializeIgnore] public int ID - { - get => Episode.ID; - set => Episode.ID = value; - } - - /// - [SerializeIgnore] public string Slug => Episode.Slug; - - /// - /// The episode currently watched - /// - public Episode Episode { get; set; } - - /// - /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). - /// - public int WatchedPercentage { get; set; } - } -} \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 4a4b416b..584230b9 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -73,6 +73,11 @@ namespace Kyoo /// All people's role. See . /// public DbSet PeopleRoles { get; set; } + + /// + /// Episodes with a watch percentage. See + /// + public DbSet WatchedEpisodes { get; set; } /// /// Get a generic link between two resource types. @@ -188,6 +193,17 @@ namespace Kyoo .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); + modelBuilder.Entity() + .HasMany(x => x.Watched) + .WithMany("users") + .UsingEntity>( + y => y + .HasOne(x => x.Second) + .WithMany(), + y => y + .HasOne(x => x.First) + .WithMany(x => x.ShowLinks), + y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasOne(x => x.Show) @@ -210,6 +226,9 @@ namespace Kyoo .WithMany(x => x.MetadataLinks) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasKey(x => new {First = x.FirstID, Second = x.SecondID}); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); @@ -217,6 +236,7 @@ namespace Kyoo modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity() .HasIndex(x => x.Slug) @@ -248,6 +268,9 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); } /// diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 56479e95..c9067984 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -2,17 +2,20 @@ net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + + + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql false false false false true - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo - default diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs similarity index 87% rename from Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs index 20ac842f..834321b2 100644 --- a/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Kyoo.Models; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -11,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Postgresql.Migrations { [DbContext(typeof(PostgresContext))] - [Migration("20210505182627_Initial")] + [Migration("20210507203809_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -225,6 +226,21 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Link"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("ID") @@ -509,6 +525,58 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Tracks"); }); + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text"); + + b.Property>("ExtraData") + .HasColumnType("jsonb"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("WatchedPercentage") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + modelBuilder.Entity("Kyoo.Models.Episode", b => { b.HasOne("Kyoo.Models.Season", "Season") @@ -621,6 +689,25 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.HasOne("Kyoo.Models.Episode", "Episode") @@ -710,6 +797,25 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Episode"); }); + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + modelBuilder.Entity("Kyoo.Models.Collection", b => { b.Navigation("LibraryLinks"); @@ -780,6 +886,13 @@ namespace Kyoo.Postgresql.Migrations { b.Navigation("Shows"); }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); #pragma warning restore 612, 618 } } diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs similarity index 86% rename from Kyoo.Postgresql/Migrations/20210505182627_Initial.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.cs index eacd436d..678e90ee 100644 --- a/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Kyoo.Models; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -104,6 +105,24 @@ namespace Kyoo.Postgresql.Migrations table.PrimaryKey("PK_Studios", x => x.ID); }); + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + ID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Username = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + Password = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text[]", nullable: true), + ExtraData = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.ID); + }); + migrationBuilder.CreateTable( name: "Link", columns: table => new @@ -256,6 +275,30 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Users_FirstID", + column: x => x.FirstID, + principalTable: "Users", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "PeopleRoles", columns: table => new @@ -420,6 +463,31 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "WatchedEpisodes", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), + WatchedPercentage = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_WatchedEpisodes_Episodes_SecondID", + column: x => x.SecondID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_WatchedEpisodes_Users_FirstID", + column: x => x.FirstID, + principalTable: "Users", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX_Collections_Slug", table: "Collections", @@ -474,6 +542,11 @@ namespace Kyoo.Postgresql.Migrations table: "Link", column: "SecondID"); + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + migrationBuilder.CreateIndex( name: "IX_MetadataIds_EpisodeID", table: "MetadataIds", @@ -549,6 +622,17 @@ namespace Kyoo.Postgresql.Migrations table: "Tracks", columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Slug", + table: "Users", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WatchedEpisodes_SecondID", + table: "WatchedEpisodes", + column: "SecondID"); } protected override void Down(MigrationBuilder migrationBuilder) @@ -568,6 +652,9 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.DropTable( name: "Link"); + migrationBuilder.DropTable( + name: "Link"); + migrationBuilder.DropTable( name: "MetadataIds"); @@ -577,6 +664,9 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.DropTable( name: "Tracks"); + migrationBuilder.DropTable( + name: "WatchedEpisodes"); + migrationBuilder.DropTable( name: "Collections"); @@ -595,6 +685,9 @@ namespace Kyoo.Postgresql.Migrations migrationBuilder.DropTable( name: "Episodes"); + migrationBuilder.DropTable( + name: "Users"); + migrationBuilder.DropTable( name: "Seasons"); diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 05d8772d..4c6ceac7 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Kyoo.Models; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -223,6 +224,21 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Link"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("ID") @@ -507,6 +523,58 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Tracks"); }); + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text"); + + b.Property>("ExtraData") + .HasColumnType("jsonb"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("WatchedPercentage") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + modelBuilder.Entity("Kyoo.Models.Episode", b => { b.HasOne("Kyoo.Models.Season", "Season") @@ -619,6 +687,25 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.HasOne("Kyoo.Models.Episode", "Episode") @@ -708,6 +795,25 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Episode"); }); + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + modelBuilder.Entity("Kyoo.Models.Collection", b => { b.Navigation("LibraryLinks"); @@ -778,6 +884,13 @@ namespace Kyoo.Postgresql.Migrations { b.Navigation("Shows"); }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); #pragma warning restore 612, 618 } } diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index b5e4febe..4836601c 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -89,6 +89,10 @@ namespace Kyoo.Postgresql modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); + + modelBuilder.Entity() + .Property(x => x.ExtraData) + .HasColumnType("jsonb"); base.OnModelCreating(modelBuilder); } diff --git a/Kyoo.WebApp b/Kyoo.WebApp index da35a725..6802bc11 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3 +Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99 diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileManager.cs index 3fd3cf75..43b808b8 100644 --- a/Kyoo/Controllers/FileManager.cs +++ b/Kyoo/Controllers/FileManager.cs @@ -8,10 +8,22 @@ using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Controllers { + /// + /// A for the local filesystem (using System.IO). + /// public class FileManager : IFileManager { + /// + /// An extension provider to get content types from files extensions. + /// private FileExtensionContentTypeProvider _provider; + /// + /// Get the content type of a file using it's extension. + /// + /// The path of the file + /// The extension of the file is not known. + /// The content type of the file private string _GetContentType(string path) { if (_provider == null) @@ -28,26 +40,36 @@ namespace Kyoo.Controllers throw new NotImplementedException($"Can't get the content type of the file at: {path}"); } - // TODO add a way to force content type - public IActionResult FileResult(string path, bool range) + /// + public IActionResult FileResult(string path, bool range = false, string type = null) { if (path == null) return new NotFoundResult(); if (!File.Exists(path)) return new NotFoundResult(); - return new PhysicalFileResult(Path.GetFullPath(path), _GetContentType(path)) + return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path)) { EnableRangeProcessing = range }; } - public StreamReader GetReader(string path) + /// + public Stream GetReader(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return new StreamReader(path); + return File.OpenRead(path); } + /// + public Stream NewFile(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return File.Create(path); + } + + /// public Task> ListFiles(string path) { if (path == null) @@ -57,11 +79,13 @@ namespace Kyoo.Controllers : Array.Empty()); } + /// public Task Exists(string path) { return Task.FromResult(File.Exists(path)); } + /// public string GetExtraDirectory(Show show) { string path = Path.Combine(show.Path, "Extra"); @@ -69,6 +93,7 @@ namespace Kyoo.Controllers return path; } + /// public string GetExtraDirectory(Season season) { if (season.Show == null) @@ -79,6 +104,7 @@ namespace Kyoo.Controllers return path; } + /// public string GetExtraDirectory(Episode episode) { string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra"); diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 41a1bb0a..2498e607 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -14,7 +14,7 @@ namespace Kyoo.Controllers public class ShowRepository : LocalRepository, IShowRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; /// diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index f7d5dea5..c2d5e321 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -44,7 +44,8 @@ namespace Kyoo (typeof(IPeopleRepository), typeof(DatabaseContext)), (typeof(IStudioRepository), typeof(DatabaseContext)), (typeof(IGenreRepository), typeof(DatabaseContext)), - (typeof(IProviderRepository), typeof(DatabaseContext)) + (typeof(IProviderRepository), typeof(DatabaseContext)), + (typeof(IUserRepository), typeof(DatabaseContext)) }; /// @@ -88,6 +89,7 @@ namespace Kyoo services.AddRepository(); services.AddRepository(); services.AddRepository(); + services.AddRepository(); } services.AddTask(); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 33eb9a67..3c7ac57e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -126,14 +126,14 @@ namespace Kyoo app.UseResponseCompression(); _plugins.ConfigureAspnet(app); - - app.UseSpa(spa => - { - spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - - if (env.IsDevelopment()) - spa.UseAngularCliServer("start"); - }); + // + // app.UseSpa(spa => + // { + // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); + // + // if (env.IsDevelopment()) + // spa.UseAngularCliServer("start"); + // }); app.UseEndpoints(endpoints => { diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 4ae053de..7052d9eb 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -71,7 +71,7 @@ namespace Kyoo.Api await writer.WriteLineAsync(""); await writer.WriteLineAsync(""); - using StreamReader reader = _files.GetReader(_path); + using StreamReader reader = new(_files.GetReader(_path)); string line; while ((line = await reader.ReadLineAsync()) != null) { From 429af9b2524494ab13eb3b289959f08bafb75ce4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 8 May 2021 17:55:22 +0200 Subject: [PATCH 18/25] Fixing User database gestion --- Kyoo.Authentication/AuthenticationModule.cs | 4 ++ .../Models/DTO/AccountUpdateRequest.cs | 4 +- .../Models/DTO/RegisterRequest.cs | 10 +++-- ...tionOptions.cs => AuthenticationOption.cs} | 2 +- Kyoo.Authentication/Views/AccountApi.cs | 44 ++++++++++++------- Kyoo.CommonAPI/DatabaseContext.cs | 1 + Kyoo.Postgresql/PostgresModule.cs | 2 +- Kyoo.WebLogin/login.js | 20 ++++----- .../Repositories/UserRepository.cs | 9 ++++ Kyoo/Kyoo.csproj | 2 +- Kyoo/Startup.cs | 22 +++++----- Kyoo/settings.json | 6 ++- 12 files changed, 80 insertions(+), 46 deletions(-) rename Kyoo.Authentication/Models/Options/{AuthenticationOptions.cs => AuthenticationOption.cs} (94%) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 02a0f982..559517d2 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -70,6 +70,8 @@ namespace Kyoo.Authentication { string publicUrl = _configuration.GetValue("public_url").TrimEnd('/'); + services.AddControllers(); + // services.AddDbContext(options => // { // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); @@ -84,6 +86,8 @@ namespace Kyoo.Authentication // .AddEntityFrameworkStores(); services.Configure(_configuration.GetSection(PermissionOption.Path)); + services.Configure(_configuration.GetSection(CertificateOption.Path)); + services.Configure(_configuration.GetSection(AuthenticationOption.Path)); CertificateOption certificateOptions = new(); _configuration.GetSection(CertificateOption.Path).Bind(certificateOptions); diff --git a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs index 4ec474d5..ac135799 100644 --- a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs +++ b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs @@ -11,13 +11,13 @@ namespace Kyoo.Authentication.Models.DTO /// /// The new email address of the user /// - [EmailAddress] + [EmailAddress(ErrorMessage = "The email is invalid.")] public string Email { get; set; } /// /// The new username of the user. /// - [MinLength(4)] + [MinLength(4, ErrorMessage = "The username must have at least 4 characters")] public string Username { get; set; } /// diff --git a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs index 4d8efa2c..ad556f6d 100644 --- a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs +++ b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Kyoo.Models; @@ -11,19 +12,19 @@ namespace Kyoo.Authentication.Models.DTO /// /// The user email address /// - [EmailAddress] + [EmailAddress(ErrorMessage = "The email must be a valid email address")] public string Email { get; set; } /// /// The user's username. /// - [MinLength(4)] + [MinLength(4, ErrorMessage = "The username must have at least {1} characters")] public string Username { get; set; } /// /// The user's password. /// - [MinLength(8)] + [MinLength(8, ErrorMessage = "The password must have at least {1} characters")] public string Password { get; set; } @@ -38,7 +39,8 @@ namespace Kyoo.Authentication.Models.DTO Slug = Utility.ToSlug(Username), Username = Username, Password = Password, - Email = Email + Email = Email, + ExtraData = new Dictionary() }; } } diff --git a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs similarity index 94% rename from Kyoo.Authentication/Models/Options/AuthenticationOptions.cs rename to Kyoo.Authentication/Models/Options/AuthenticationOption.cs index f35e22a3..23e917aa 100644 --- a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs +++ b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs @@ -3,7 +3,7 @@ namespace Kyoo.Authentication.Models /// /// The main authentication options. /// - public class AuthenticationOptions + public class AuthenticationOption { /// /// The path to get this option from the root configuration. diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 903c7e93..bd49da94 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -2,18 +2,20 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Threading.Tasks; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; +using Kyoo.Authentication.Models; using Kyoo.Authentication.Models.DTO; using Kyoo.Controllers; using Kyoo.Models; +using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions; namespace Kyoo.Authentication.Views { @@ -32,7 +34,7 @@ namespace Kyoo.Authentication.Views /// /// The identity server interaction service to login users. /// - private readonly IIdentityServerInteractionService _interaction; + // private readonly IIdentityServerInteractionService _interaction; /// /// A file manager to send profile pictures /// @@ -41,7 +43,7 @@ namespace Kyoo.Authentication.Views /// /// Options about authentication. Those options are monitored and reloads are supported. /// - private readonly IOptionsMonitor _options; + private readonly IOptions _options; /// @@ -52,12 +54,12 @@ namespace Kyoo.Authentication.Views /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, - IIdentityServerInteractionService interaction, + // IIdentityServerInteractionService interaction, IFileManager files, - IOptionsMonitor options) + IOptions options) { _users = users; - _interaction = interaction; + // _interaction = interaction; _files = files; _options = options; } @@ -69,15 +71,23 @@ namespace Kyoo.Authentication.Views /// The DTO register request /// A OTAC to connect to this new account [HttpPost("register")] - public async Task> Register([FromBody] RegisterRequest request) + public async Task Register([FromBody] RegisterRequest request) { User user = request.ToUser(); - user.Permissions = _options.CurrentValue.Permissions.NewUser; + user.Permissions = _options.Value.Permissions.NewUser; user.Password = PasswordUtils.HashPassword(user.Password); user.ExtraData["otac"] = PasswordUtils.GenerateOTAC(); user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s"); - await _users.Create(user); - return user.ExtraData["otac"]; + try + { + await _users.Create(user); + } + catch (DuplicatedItemException) + { + return Conflict(new {Errors = new {Duplicate = new[] {"A user with this name already exists"}}}); + } + + return Ok(new {Otac = user.ExtraData["otac"]}); } /// @@ -103,10 +113,10 @@ namespace Kyoo.Authentication.Views [HttpPost("login")] public async Task Login([FromBody] LoginRequest login) { - AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); + // AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); User user = await _users.Get(x => x.Username == login.Username); - if (context == null || user == null) + if (user == null) return Unauthorized(); if (!PasswordUtils.CheckPassword(login.Password, user.Password)) return Unauthorized(); @@ -122,7 +132,9 @@ namespace Kyoo.Authentication.Views [HttpPost("otac-login")] public async Task OtacLogin([FromBody] OtacRequest otac) { - User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac); + // TODO once hstore (Dictionary accessor) are supported, use them. + // We retrieve all users, this is inefficient. + User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac); if (user == null) return Unauthorized(); if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow) @@ -166,7 +178,7 @@ namespace Kyoo.Authentication.Views User user = await _users.GetOrDefault(slug); if (user == null) return NotFound(); - string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); + string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); return _files.FileResult(path); } @@ -182,7 +194,7 @@ namespace Kyoo.Authentication.Views user.Username = data.Username; if (data.Picture?.Length > 0) { - string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); + string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); await using Stream file = _files.NewFile(path); await data.Picture.CopyToAsync(file); } @@ -192,7 +204,7 @@ namespace Kyoo.Authentication.Views [HttpGet("permissions")] public ActionResult> GetDefaultPermissions() { - return _options.CurrentValue.Permissions.Default; + return _options.Value.Permissions.Default; } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 584230b9..6b1bac47 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -8,6 +8,7 @@ using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace Kyoo { diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 7a818296..506f6dbe 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -63,7 +63,7 @@ namespace Kyoo.Postgresql services.AddDbContext(x => { x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); - if (_environment.IsDevelopment()) + if (_configuration.GetValue("logging:dotnet-ef")) x.EnableDetailedErrors().EnableSensitiveDataLogging(); }); // services.AddScoped(_ => new PostgresContext( diff --git a/Kyoo.WebLogin/login.js b/Kyoo.WebLogin/login.js index 36ff3697..973128c8 100644 --- a/Kyoo.WebLogin/login.js +++ b/Kyoo.WebLogin/login.js @@ -41,11 +41,11 @@ $("#login-btn").on("click", function (e) success: function () { let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl"); - + if (returnUrl == null) window.location.href = "/unauthorized"; else - window.location.href = returnUrl; + window.location.href = returnUrl; }, error: function(xhr) { @@ -56,7 +56,7 @@ $("#login-btn").on("click", function (e) }); }); -$("#register-btn").on("click", function (e) +$("#register-btn").on("click", function (e) { e.preventDefault(); @@ -73,7 +73,7 @@ $("#register-btn").on("click", function (e) error.text("Passwords don't match."); return; } - + $.ajax( { url: "/api/account/register", @@ -81,19 +81,19 @@ $("#register-btn").on("click", function (e) contentType: 'application/json;charset=UTF-8', dataType: 'json', data: JSON.stringify(user), - success: function(res) + success: function(res) { useOtac(res.otac); }, - error: function(xhr) + error: function(xhr) { let error = $("#register-error"); error.show(); - error.text(JSON.parse(xhr.responseText)[0].description); + error.html(Object.values(JSON.parse(xhr.responseText).errors).map(x => x[0]).join("
")); } }); }); - + function useOtac(otac) { $.ajax( @@ -101,7 +101,7 @@ function useOtac(otac) url: "/api/account/otac-login", type: "POST", contentType: 'application/json;charset=UTF-8', - data: JSON.stringify({otac: otac, tayLoggedIn: $("#stay-logged-in")[0].checked}), + data: JSON.stringify({otac: otac, stayLoggedIn: $("#stay-logged-in")[0].checked}), success: function() { let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl"); @@ -124,4 +124,4 @@ function useOtac(otac) let otac = new URLSearchParams(window.location.search).get("otac"); if (otac != null) - useOtac(otac); \ No newline at end of file + useOtac(otac); diff --git a/Kyoo/Controllers/Repositories/UserRepository.cs b/Kyoo/Controllers/Repositories/UserRepository.cs index 97bce050..5ebd4d2b 100644 --- a/Kyoo/Controllers/Repositories/UserRepository.cs +++ b/Kyoo/Controllers/Repositories/UserRepository.cs @@ -41,6 +41,15 @@ namespace Kyoo.Controllers .Take(20) .ToListAsync(); } + + /// + public override async Task Create(User obj) + { + await base.Create(obj); + _database.Entry(obj).State = EntityState.Added; + await _database.SaveChangesAsync($"Trying to insert a duplicated user (slug {obj.Slug} already exists)."); + return obj; + } /// public override async Task Delete(User obj) diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5804420f..7c39d8d3 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -45,7 +45,7 @@ - all + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 3c7ac57e..0054c066 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Postgresql; @@ -46,7 +47,7 @@ namespace Kyoo _configuration = configuration; _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); - _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), new AuthenticationModule(configuration, loggerFactory)}); } /// @@ -126,19 +127,20 @@ namespace Kyoo app.UseResponseCompression(); _plugins.ConfigureAspnet(app); - // - // app.UseSpa(spa => - // { - // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - // - // if (env.IsDevelopment()) - // spa.UseAngularCliServer("start"); - // }); - + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); }); + + + app.UseSpa(spa => + { + spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); + + if (env.IsDevelopment()) + spa.UseAngularCliServer("start"); + }); } } } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index b521f8a8..cc213cc0 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -20,8 +20,12 @@ "default": "Trace", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.DbUpdateException": "None", + "Microsoft.EntityFrameworkCore.Update": "None", + "Microsoft.EntityFrameworkCore.Database.Command": "None", "Kyoo": "Trace" - } + }, + "dotnet-ef": "false" }, "authentication": { From d7972704dd7374c8dbc3b089a6bbbfe39057495b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 May 2021 02:38:54 +0200 Subject: [PATCH 19/25] Working oidc --- Kyoo.Authentication/AuthenticationModule.cs | 50 +++++-- .../AuthorizationValidatorHandler.cs | 2 +- Kyoo.Authentication/Controllers/UserStore.cs | 133 ++++++++++++++++++ Kyoo.Authentication/IdentityContext.cs | 12 +- .../Models/Options/PermissionOption.cs | 2 - Kyoo.Authentication/Views/AccountApi.cs | 35 +++-- Kyoo.Common/Controllers/ILibraryManager.cs | 20 ++- Kyoo.Common/Controllers/IRepository.cs | 3 +- .../Exceptions/DuplicatedItemException.cs | 10 ++ .../Exceptions/ItemNotFoundException.cs | 10 ++ Kyoo.Common/Models/LibraryItem.cs | 3 +- Kyoo.Common/Models/Link.cs | 2 + Kyoo.Common/Models/MetadataID.cs | 9 -- Kyoo.Common/Models/PeopleRole.cs | 23 --- Kyoo.Common/Models/Resources/Episode.cs | 43 ------ Kyoo.Common/Models/Resources/Library.cs | 11 -- Kyoo.Common/Models/Resources/People.cs | 11 -- Kyoo.Common/Models/Resources/Season.cs | 20 --- Kyoo.Common/Models/Resources/Show.cs | 56 -------- Kyoo.CommonAPI/CrudApi.cs | 22 +-- Kyoo.CommonAPI/DatabaseContext.cs | 1 - Kyoo.CommonAPI/LocalRepository.cs | 14 +- Kyoo.Postgresql/PostgresModule.cs | 2 +- Kyoo.Tests/Library/SetupTests.cs | 3 - Kyoo.WebApp | 2 +- .../Repositories/EpisodeRepository.cs | 2 +- .../Repositories/LibraryItemRepository.cs | 7 +- .../Repositories/LibraryRepository.cs | 5 +- .../Repositories/PeopleRepository.cs | 12 +- .../Repositories/SeasonRepository.cs | 2 +- .../Repositories/ShowRepository.cs | 8 +- .../Repositories/TrackRepository.cs | 3 +- Kyoo/Program.cs | 5 +- Kyoo/Startup.cs | 3 +- Kyoo/Tasks/Crawler.cs | 21 ++- Kyoo/Views/CollectionApi.cs | 4 +- Kyoo/Views/EpisodeApi.cs | 14 +- Kyoo/Views/GenreApi.cs | 3 +- Kyoo/Views/LibraryApi.cs | 9 +- Kyoo/Views/PeopleApi.cs | 10 +- Kyoo/Views/ProviderApi.cs | 11 +- Kyoo/Views/SeasonApi.cs | 17 ++- Kyoo/Views/ShowApi.cs | 27 ++-- Kyoo/Views/StudioApi.cs | 4 +- Kyoo/settings.json | 9 +- 45 files changed, 347 insertions(+), 328 deletions(-) create mode 100644 Kyoo.Authentication/Controllers/UserStore.cs diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 559517d2..26b50dd1 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -3,16 +3,19 @@ using System.Collections.Generic; using IdentityServer4.Extensions; using IdentityServer4.Services; using Kyoo.Authentication.Models; +using Kyoo.Authentication.Views; using Kyoo.Controllers; -using Kyoo.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Logging; namespace Kyoo.Authentication { @@ -53,16 +56,25 @@ namespace Kyoo.Authentication /// private readonly ILoggerFactory _loggerFactory; - + /// + /// The environment information to check if the app runs in debug mode + /// + private readonly IWebHostEnvironment _environment; + + /// /// Create a new authentication module instance and use the given configuration and environment. /// /// The configuration to use /// The logger factory to allow IdentityServer to log things - public AuthenticationModule(IConfiguration configuration, ILoggerFactory loggerFactory) + /// The environment information to check if the app runs in debug mode + public AuthenticationModule(IConfiguration configuration, + ILoggerFactory loggerFactory, + IWebHostEnvironment environment) { _configuration = configuration; _loggerFactory = loggerFactory; + _environment = environment; } /// @@ -70,8 +82,16 @@ namespace Kyoo.Authentication { string publicUrl = _configuration.GetValue("public_url").TrimEnd('/'); + if (_environment.IsDevelopment()) + IdentityModelEventSource.ShowPII = true; + services.AddControllers(); + // services.AddIdentityCore() + // .AddSignInManager() + // .AddDefaultTokenProviders() + // .AddUserStore(); + // services.AddDbContext(options => // { // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); @@ -113,25 +133,25 @@ namespace Kyoo.Authentication // options.EnableTokenCleanup = true; // }) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) + .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) .AddInMemoryClients(IdentityContext.GetClients()) - .AddDeveloperSigningCredential(); - // .AddProfileService() - // .AddSigninKeys(certificateOptions); + .AddProfileService() + .AddSigninKeys(certificateOptions); // TODO implement means to add clients or api scopes for other plugins. // TODO split scopes (kyoo.read should be task.read, video.read etc) - services.AddAuthentication(o => - { - o.DefaultScheme = IdentityConstants.ApplicationScheme; - o.DefaultSignInScheme = IdentityConstants.ExternalScheme; - }) - .AddIdentityCookies(_ => { }); + // services.AddAuthentication(o => + // { + // o.DefaultScheme = IdentityConstants.ApplicationScheme; + // o.DefaultSignInScheme = IdentityConstants.ExternalScheme; + // }) + // .AddIdentityCookies(_ => { }); services.AddAuthentication() .AddJwtBearer(options => { options.Authority = publicUrl; - options.Audience = "Kyoo"; + options.Audience = "kyoo"; options.RequireHttpsMetadata = false; }); @@ -146,10 +166,10 @@ namespace Kyoo.Authentication { options.AddPolicy(permission, policy => { - policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme); + // policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme); policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); policy.AddRequirements(new AuthRequirement(permission)); - policy.RequireScope($"kyoo.{permission.ToLower()}"); + // policy.RequireScope($"kyoo.{permission.ToLower()}"); }); } }); diff --git a/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs b/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs index 00df30db..1df1c86a 100644 --- a/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs +++ b/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs @@ -41,7 +41,7 @@ namespace Kyoo.Authentication else { ICollection defaultPerms = _options.CurrentValue.Default; - if (defaultPerms.Contains(requirement.Permission.ToLower())) + if (defaultPerms?.Contains(requirement.Permission.ToLower()) == true) context.Succeed(requirement); } diff --git a/Kyoo.Authentication/Controllers/UserStore.cs b/Kyoo.Authentication/Controllers/UserStore.cs new file mode 100644 index 00000000..0649796f --- /dev/null +++ b/Kyoo.Authentication/Controllers/UserStore.cs @@ -0,0 +1,133 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.AspNetCore.Identity; + +namespace Kyoo.Authentication +{ + /// + /// An implementation of an that uses an . + /// + public class UserStore : IUserStore + { + /// + /// The user repository used to store users. + /// + private readonly IUserRepository _users; + + /// + /// Create a new . + /// + /// The user repository to use + public UserStore(IUserRepository users) + { + _users = users; + } + + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Implementation of the IDisposable pattern + /// + /// True if this class should be disposed. + protected virtual void Dispose(bool disposing) + { + bool _ = disposing; + // Not implemented because this class has nothing to dispose. + } + + /// + public Task GetUserIdAsync(User user, CancellationToken cancellationToken) + { + return Task.FromResult(user.ID.ToString()); + } + + /// + public Task GetUserNameAsync(User user, CancellationToken cancellationToken) + { + return Task.FromResult(user.Username); + } + + /// + public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken) + { + user.Username = userName; + return Task.CompletedTask; + } + + /// + public Task GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken) + { + return Task.FromResult(user.Slug); + } + + /// + public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken) + { + user.Slug = normalizedName; + return Task.CompletedTask; + } + + /// + public async Task CreateAsync(User user, CancellationToken cancellationToken) + { + try + { + await _users.Create(user); + return IdentityResult.Success; + } + catch (Exception ex) + { + return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); + } + } + + /// + public async Task UpdateAsync(User user, CancellationToken cancellationToken) + { + try + { + await _users.Edit(user, false); + return IdentityResult.Success; + } + catch (Exception ex) + { + return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); + } + } + + /// + public async Task DeleteAsync(User user, CancellationToken cancellationToken) + { + try + { + await _users.Delete(user); + return IdentityResult.Success; + } + catch (Exception ex) + { + return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); + } + } + + /// + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + return _users.GetOrDefault(int.Parse(userId)); + } + + /// + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + return _users.GetOrDefault(normalizedUserName); + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/IdentityContext.cs b/Kyoo.Authentication/IdentityContext.cs index 21d649e1..071a601b 100644 --- a/Kyoo.Authentication/IdentityContext.cs +++ b/Kyoo.Authentication/IdentityContext.cs @@ -23,7 +23,7 @@ namespace Kyoo.Authentication new() { ClientId = "kyoo.webapp", - + AccessTokenType = AccessTokenType.Jwt, AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, @@ -33,7 +33,7 @@ namespace Kyoo.Authentication AllowOfflineAccess = true, RequireConsent = false, - AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.download", "kyoo.admin" }, + AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.admin" }, RedirectUris = { "/", "/silent.html" }, PostLogoutRedirectUris = { "/logout" } } @@ -60,11 +60,6 @@ namespace Kyoo.Authentication DisplayName = "Allow playback of movies and episodes." }, new ApiScope - { - Name = "kyoo.download", - DisplayName = "Allow downloading of episodes and movies from kyoo." - }, - new ApiScope { Name = "kyoo.admin", DisplayName = "Full access to the admin's API and the public API." @@ -76,9 +71,8 @@ namespace Kyoo.Authentication { return new[] { - new ApiResource + new ApiResource("kyoo", "Kyoo") { - Name = "Kyoo", Scopes = GetScopes().Select(x => x.Name).ToArray() } }; diff --git a/Kyoo.Authentication/Models/Options/PermissionOption.cs b/Kyoo.Authentication/Models/Options/PermissionOption.cs index 0a26f89e..8d6c698d 100644 --- a/Kyoo.Authentication/Models/Options/PermissionOption.cs +++ b/Kyoo.Authentication/Models/Options/PermissionOption.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Kyoo.Authentication.Models { /// diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index bd49da94..81bc3a55 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; +using IdentityServer4; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; @@ -14,6 +16,7 @@ using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -39,6 +42,7 @@ namespace Kyoo.Authentication.Views /// A file manager to send profile pictures /// private readonly IFileManager _files; + // private readonly SignInManager _signInManager; /// /// Options about authentication. Those options are monitored and reloads are supported. @@ -57,11 +61,13 @@ namespace Kyoo.Authentication.Views // IIdentityServerInteractionService interaction, IFileManager files, IOptions options) + //, SignInManager signInManager) { _users = users; // _interaction = interaction; _files = files; _options = options; + // _signInManager = signInManager; } @@ -114,14 +120,14 @@ namespace Kyoo.Authentication.Views public async Task Login([FromBody] LoginRequest login) { // AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); - User user = await _users.Get(x => x.Username == login.Username); + User user = await _users.GetOrDefault(x => x.Username == login.Username); if (user == null) return Unauthorized(); if (!PasswordUtils.CheckPassword(login.Password, user.Password)) return Unauthorized(); - await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(login.StayLoggedIn)); + // await _signInManager.SignInAsync(user, login.StayLoggedIn); return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true }); } @@ -142,7 +148,15 @@ namespace Kyoo.Authentication.Views { code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password." }); - await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(otac.StayLoggedIn)); + + + IdentityServerUser iduser = new(user.ID.ToString()) + { + DisplayName = user.Username + }; + + await HttpContext.SignInAsync(iduser, StayLogged(otac.StayLoggedIn)); + // await _signInManager.SignInAsync(user, otac.StayLoggedIn); return Ok(); } @@ -153,22 +167,23 @@ namespace Kyoo.Authentication.Views [Authorize] public async Task Logout() { - await HttpContext.SignOutAsync(); + // await _signInManager.SignOutAsync(); return Ok(); } // TODO check with the extension method public async Task GetProfileDataAsync(ProfileDataRequestContext context) { - User user = await _users.Get(int.Parse(context.Subject.GetSubjectId())); + User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); if (user == null) return; context.IssuedClaims.AddRange(user.GetClaims()); + context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions))); } public async Task IsActiveAsync(IsActiveContext context) { - User user = await _users.Get(int.Parse(context.Subject.GetSubjectId())); + User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); context.IsActive = user != null; } @@ -186,8 +201,10 @@ namespace Kyoo.Authentication.Views [Authorize] public async Task> Update([FromForm] AccountUpdateRequest data) { - User user = await _users.Get(int.Parse(HttpContext.User.GetSubjectId())); - + User user = await _users.GetOrDefault(int.Parse(HttpContext.User.GetSubjectId())); + + if (user == null) + return Unauthorized(); if (!string.IsNullOrEmpty(data.Email)) user.Email = data.Email; if (!string.IsNullOrEmpty(data.Username)) @@ -204,7 +221,7 @@ namespace Kyoo.Authentication.Views [HttpGet("permissions")] public ActionResult> GetDefaultPermissions() { - return _options.Value.Permissions.Default; + return _options.Value.Permissions.Default ?? Array.Empty(); } } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 6c100622..2cd0c909 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -84,6 +84,7 @@ namespace Kyoo.Controllers /// The type of the resource /// If the item is not found /// The resource found + [ItemNotNull] Task Get(int id) where T : class, IResource; /// @@ -93,6 +94,7 @@ namespace Kyoo.Controllers /// The type of the resource /// If the item is not found /// The resource found + [ItemNotNull] Task Get(string slug) where T : class, IResource; /// @@ -102,6 +104,7 @@ namespace Kyoo.Controllers /// The type of the resource /// If the item is not found /// The first resource found that match the where function + [ItemNotNull] Task Get(Expression> where) where T : class, IResource; /// @@ -111,6 +114,7 @@ namespace Kyoo.Controllers /// The season's number /// If the item is not found /// The season found + [ItemNotNull] Task Get(int showID, int seasonNumber); /// @@ -120,6 +124,7 @@ namespace Kyoo.Controllers /// The season's number /// If the item is not found /// The season found + [ItemNotNull] Task Get(string showSlug, int seasonNumber); /// @@ -130,6 +135,7 @@ namespace Kyoo.Controllers /// The episode's number /// If the item is not found /// The episode found + [ItemNotNull] Task Get(int showID, int seasonNumber, int episodeNumber); /// @@ -140,6 +146,7 @@ namespace Kyoo.Controllers /// The episode's number /// If the item is not found /// The episode found + [ItemNotNull] Task Get(string showSlug, int seasonNumber, int episodeNumber); /// @@ -148,7 +155,8 @@ namespace Kyoo.Controllers /// The slug of the track /// The type (Video, Audio or Subtitle) /// If the item is not found - /// The tracl found + /// The track found + [ItemNotNull] Task Get(string slug, StreamType type = StreamType.Unknown); /// @@ -157,6 +165,7 @@ namespace Kyoo.Controllers /// The id of the resource /// The type of the resource /// The resource found + [ItemCanBeNull] Task GetOrDefault(int id) where T : class, IResource; /// @@ -165,6 +174,7 @@ namespace Kyoo.Controllers /// The slug of the resource /// The type of the resource /// The resource found + [ItemCanBeNull] Task GetOrDefault(string slug) where T : class, IResource; /// @@ -173,6 +183,7 @@ namespace Kyoo.Controllers /// The filter function. /// The type of the resource /// The first resource found that match the where function + [ItemCanBeNull] Task GetOrDefault(Expression> where) where T : class, IResource; /// @@ -181,6 +192,7 @@ namespace Kyoo.Controllers /// The id of the show /// The season's number /// The season found + [ItemCanBeNull] Task GetOrDefault(int showID, int seasonNumber); /// @@ -189,6 +201,7 @@ namespace Kyoo.Controllers /// The slug of the show /// The season's number /// The season found + [ItemCanBeNull] Task GetOrDefault(string showSlug, int seasonNumber); /// @@ -198,6 +211,7 @@ namespace Kyoo.Controllers /// The season's number /// The episode's number /// The episode found + [ItemCanBeNull] Task GetOrDefault(int showID, int seasonNumber, int episodeNumber); /// @@ -207,6 +221,7 @@ namespace Kyoo.Controllers /// The season's number /// The episode's number /// The episode found + [ItemCanBeNull] Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); /// @@ -214,7 +229,8 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// The tracl found + /// The track found + [ItemCanBeNull] Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 4e8b16a3..dad2d5e3 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -212,9 +212,8 @@ namespace Kyoo.Controllers /// Create a new resource if it does not exist already. If it does, the existing value is returned instead. /// /// The object to create - /// Allow issues to occurs in this method. Every issue is caught and ignored. /// The newly created item or the existing value if it existed. - Task CreateIfNotExists([NotNull] T obj, bool silentFail = false); + Task CreateIfNotExists([NotNull] T obj); /// /// Edit a resource diff --git a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs index eb85432f..b0d26bf0 100644 --- a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs +++ b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace Kyoo.Models.Exceptions { @@ -22,5 +23,14 @@ namespace Kyoo.Models.Exceptions public DuplicatedItemException(string message) : base(message) { } + + /// + /// The serialization constructor + /// + /// Serialization infos + /// The serialization context + protected DuplicatedItemException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs b/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs index a04b60a1..d05882b1 100644 --- a/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs +++ b/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace Kyoo.Models.Exceptions { @@ -20,5 +21,14 @@ namespace Kyoo.Models.Exceptions public ItemNotFoundException(string message) : base(message) { } + + /// + /// The serialization constructor + /// + /// Serialization infos + /// The serialization context + protected ItemNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs index 6fe964d4..78f604f2 100644 --- a/Kyoo.Common/Models/LibraryItem.cs +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using JetBrains.Annotations; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -22,7 +23,7 @@ namespace Kyoo.Models public int? StartYear { get; set; } public int? EndYear { get; set; } [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } - private string _type => Type == ItemType.Collection ? "collection" : "show"; + [UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show"; public ItemType Type { get; set; } public LibraryItem() {} diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs index f504b5a0..2df85f1f 100644 --- a/Kyoo.Common/Models/Link.cs +++ b/Kyoo.Common/Models/Link.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; namespace Kyoo.Models @@ -60,6 +61,7 @@ namespace Kyoo.Models public Link() {} + [SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")] public Link(T1 first, T2 second, bool privateItems = false) : base(first, second) { diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index cc7985ac..d1752d50 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -22,14 +22,5 @@ namespace Kyoo.Models public string DataID { get; set; } public string Link { get; set; } - - public MetadataID() { } - - public MetadataID(Provider provider, string dataID, string link) - { - Provider = provider; - DataID = dataID; - Link = link; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/PeopleRole.cs b/Kyoo.Common/Models/PeopleRole.cs index fe027682..be48abd1 100644 --- a/Kyoo.Common/Models/PeopleRole.cs +++ b/Kyoo.Common/Models/PeopleRole.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -14,27 +13,5 @@ namespace Kyoo.Models [SerializeIgnore] public virtual Show Show { get; set; } public string Role { get; set; } public string Type { get; set; } - - public PeopleRole() {} - - public PeopleRole(People people, Show show, string role, string type) - { - People = people; - Show = show; - Role = role; - Type = type; - } - - public PeopleRole(string slug, - string name, - string role, - string type, - string poster, - IEnumerable externalIDs) - { - People = new People(slug, name, poster, externalIDs); - Role = role; - Type = type; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 27feefd3..29aeab06 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -32,48 +31,6 @@ namespace Kyoo.Models [EditableRelation] [LoadableRelation] public virtual ICollection Tracks { get; set; } - public Episode() { } - - public Episode(int seasonNumber, - int episodeNumber, - int absoluteNumber, - string title, - string overview, - DateTime? releaseDate, - int runtime, - string thumb, - IEnumerable externalIDs) - { - SeasonNumber = seasonNumber; - EpisodeNumber = episodeNumber; - AbsoluteNumber = absoluteNumber; - Title = title; - Overview = overview; - ReleaseDate = releaseDate; - Runtime = runtime; - Thumb = thumb; - ExternalIDs = externalIDs?.ToArray(); - } - - public Episode(int showID, - int seasonID, - int seasonNumber, - int episodeNumber, - int absoluteNumber, - string path, - string title, - string overview, - DateTime? releaseDate, - int runtime, - string poster, - IEnumerable externalIDs) - : this(seasonNumber, episodeNumber, absoluteNumber, title, overview, releaseDate, runtime, poster, externalIDs) - { - ShowID = showID; - SeasonID = seasonID; - Path = path; - } - public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber) { if (showSlug == null) diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index 86cb324d..c8148544 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -21,15 +20,5 @@ namespace Kyoo.Models [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } #endif - - public Library() { } - - public Library(string slug, string name, IEnumerable paths, IEnumerable providers) - { - Slug = slug; - Name = name; - Paths = paths?.ToArray(); - Providers = providers?.ToArray(); - } } } diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 2743cf3c..46b86143 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -13,15 +12,5 @@ namespace Kyoo.Models [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection Roles { get; set; } - - public People() {} - - public People(string slug, string name, string poster, IEnumerable externalIDs) - { - Slug = slug; - Name = name; - Poster = poster; - ExternalIDs = externalIDs?.ToArray(); - } } } diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 827b24f7..b3f7ab27 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -22,24 +21,5 @@ namespace Kyoo.Models [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } [LoadableRelation] public virtual ICollection Episodes { get; set; } - - public Season() { } - - public Season(int showID, - int seasonNumber, - string title, - string overview, - int? year, - string poster, - IEnumerable externalIDs) - { - ShowID = showID; - SeasonNumber = seasonNumber; - Title = title; - Overview = overview; - Year = year; - Poster = poster; - ExternalIDs = externalIDs?.ToArray(); - } } } diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 7b948b55..e7d14d79 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -42,62 +42,6 @@ namespace Kyoo.Models [SerializeIgnore] public virtual ICollection> GenreLinks { get; set; } #endif - - public Show() { } - - public Show(string slug, - string title, - IEnumerable aliases, - string path, string overview, - string trailerUrl, - IEnumerable genres, - Status? status, - int? startYear, - int? endYear, - IEnumerable externalIDs) - { - Slug = slug; - Title = title; - Aliases = aliases?.ToArray(); - Path = path; - Overview = overview; - TrailerUrl = trailerUrl; - Genres = genres?.ToArray(); - Status = status; - StartYear = startYear; - EndYear = endYear; - ExternalIDs = externalIDs?.ToArray(); - } - - public Show(string slug, - string title, - IEnumerable aliases, - string path, - string overview, - string trailerUrl, - Status? status, - int? startYear, - int? endYear, - string poster, - string logo, - string backdrop, - IEnumerable externalIDs) - { - Slug = slug; - Title = title; - Aliases = aliases?.ToArray(); - Path = path; - Overview = overview; - TrailerUrl = trailerUrl; - Status = status; - StartYear = startYear; - EndYear = endYear; - Poster = poster; - Logo = logo; - Backdrop = backdrop; - ExternalIDs = externalIDs?.ToArray(); - } - public string GetID(string provider) { return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 27ddf66b..f14f10db 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -29,28 +29,20 @@ namespace Kyoo.CommonApi [Authorize(Policy = "Read")] public virtual async Task> Get(int id) { - try - { - return await _repository.Get(id); - } - catch (ItemNotFoundException) - { + T ret = await _repository.GetOrDefault(id); + if (ret == null) return NotFound(); - } + return ret; } [HttpGet("{slug}")] [Authorize(Policy = "Read")] public virtual async Task> Get(string slug) { - try - { - return await _repository.Get(slug); - } - catch (ItemNotFoundException) - { + T ret = await _repository.Get(slug); + if (ret == null) return NotFound(); - } + return ret; } [HttpGet("count")] @@ -111,7 +103,7 @@ namespace Kyoo.CommonApi } catch (DuplicatedItemException) { - T existing = await _repository.Get(resource.Slug); + T existing = await _repository.GetOrDefault(resource.Slug); return Conflict(existing); } } diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 6b1bac47..584230b9 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -8,7 +8,6 @@ using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Diagnostics; namespace Kyoo { diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index ee01b089..7498fc14 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -125,7 +125,7 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default) { - return ApplyFilters(query, Get, DefaultSort, where, sort, limit); + return ApplyFilters(query, GetOrDefault, DefaultSort, where, sort, limit); } /// @@ -193,14 +193,14 @@ namespace Kyoo.Controllers } /// - public virtual async Task CreateIfNotExists(T obj, bool silentFail = false) + public virtual async Task CreateIfNotExists(T obj) { try { if (obj == null) throw new ArgumentNullException(nameof(obj)); - T old = await Get(obj.Slug); + T old = await GetOrDefault(obj.Slug); if (old != null) return old; @@ -208,13 +208,7 @@ namespace Kyoo.Controllers } catch (DuplicatedItemException) { - return await Get(obj.Slug); - } - catch - { - if (silentFail) - return default; - throw; + return await GetOrDefault(obj.Slug); } } diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 506f6dbe..7a818296 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -63,7 +63,7 @@ namespace Kyoo.Postgresql services.AddDbContext(x => { x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); - if (_configuration.GetValue("logging:dotnet-ef")) + if (_environment.IsDevelopment()) x.EnableDetailedErrors().EnableSensitiveDataLogging(); }); // services.AddScoped(_ => new PostgresContext( diff --git a/Kyoo.Tests/Library/SetupTests.cs b/Kyoo.Tests/Library/SetupTests.cs index e1852a17..ec9ed12a 100644 --- a/Kyoo.Tests/Library/SetupTests.cs +++ b/Kyoo.Tests/Library/SetupTests.cs @@ -1,6 +1,3 @@ -using System.Linq; -using Xunit; - namespace Kyoo.Tests { public class SetupTests diff --git a/Kyoo.WebApp b/Kyoo.WebApp index 6802bc11..d3a860fa 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99 +Subproject commit d3a860fa8ffccade9e3b17022482e11c9a18303e diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 91e39044..8452b950 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -234,7 +234,7 @@ namespace Kyoo.Controllers await base.Validate(resource); resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => { - x.Provider = await _providers.CreateIfNotExists(x.Provider, true); + x.Provider = await _providers.CreateIfNotExists(x.Provider); x.ProviderID = x.Provider.ID; _database.Entry(x.Provider).State = EntityState.Detached; return x; diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index ab872a50..703ece0b 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -111,12 +111,7 @@ namespace Kyoo.Controllers public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); /// - public override Task CreateIfNotExists(LibraryItem obj, bool silentFail = false) - { - if (silentFail) - return Task.FromResult(default); - throw new InvalidOperationException(); - } + public override Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); /// public override Task Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); /// diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index affd899c..d569f6fe 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -53,9 +53,8 @@ namespace Kyoo.Controllers public override async Task Create(Library obj) { await base.Create(obj); + obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToList(); _database.Entry(obj).State = EntityState.Added; - obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToArray(); - obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); return obj; } @@ -65,7 +64,7 @@ namespace Kyoo.Controllers { await base.Validate(resource); resource.Providers = await resource.Providers - .SelectAsync(x => _providers.CreateIfNotExists(x, true)) + .SelectAsync(x => _providers.CreateIfNotExists(x)) .ToListAsync(); } diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 2c9c6156..452f59eb 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -73,13 +73,13 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.Provider = await _providers.CreateIfNotExists(id.Provider); id.ProviderID = id.Provider.ID; _database.Entry(id.Provider).State = EntityState.Detached; }); await resource.Roles.ForEachAsync(async role => { - role.Show = await _shows.Value.CreateIfNotExists(role.Show, true); + role.Show = await _shows.Value.CreateIfNotExists(role.Show); role.ShowID = role.Show.ID; _database.Entry(role.Show).State = EntityState.Detached; }); @@ -129,7 +129,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.Get(showID) == null) + if (!people.Any() && await _shows.Value.GetOrDefault(showID) == null) throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; @@ -151,7 +151,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.Get(showSlug) == null) + if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null) throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; @@ -172,7 +172,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!roles.Any() && await Get(id) == null) + if (!roles.Any() && await GetOrDefault(id) == null) throw new ItemNotFoundException(); return roles; } @@ -191,7 +191,7 @@ namespace Kyoo.Controllers where, sort, limit); - if (!roles.Any() && await Get(slug) == null) + if (!roles.Any() && await GetOrDefault(slug) == null) throw new ItemNotFoundException(); return roles; } diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index a6ef7fcd..289ca08f 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -160,7 +160,7 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.Provider = await _providers.CreateIfNotExists(id.Provider); id.ProviderID = id.Provider.ID; _database.Entry(id.Provider).State = EntityState.Detached; }); diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 2498e607..eb3f36e4 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -102,22 +102,22 @@ namespace Kyoo.Controllers { await base.Validate(resource); if (resource.Studio != null) - resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true); + resource.Studio = await _studios.CreateIfNotExists(resource.Studio); resource.Genres = await resource.Genres - .SelectAsync(x => _genres.CreateIfNotExists(x, true)) + .SelectAsync(x => _genres.CreateIfNotExists(x)) .ToListAsync(); resource.GenreLinks = resource.Genres? .Select(x => Link.UCreate(resource, x)) .ToList(); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.Provider = await _providers.CreateIfNotExists(id.Provider); id.ProviderID = id.Provider.ID; _database.Entry(id.Provider).State = EntityState.Detached; }); await resource.People.ForEachAsync(async role => { - role.People = await _people.CreateIfNotExists(role.People, true); + role.People = await _people.CreateIfNotExists(role.People); role.PeopleID = role.People.ID; _database.Entry(role.People).State = EntityState.Detached; }); diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 4ce6da29..4b27a5e4 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -59,7 +59,7 @@ namespace Kyoo.Controllers if (!match.Success) { if (int.TryParse(slug, out int id)) - return Get(id); + return GetOrDefault(id); match = Regex.Match(slug, @"(?.*)\.(?.{0,3})(?-forced)?(\..*)?"); if (!match.Success) throw new ArgumentException("Invalid track slug. " + @@ -102,6 +102,7 @@ namespace Kyoo.Controllers await base.Create(obj); _database.Entry(obj).State = EntityState.Added; + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local await _database.SaveOrRetry(obj, (x, i) => { if (i > 10) diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 34ae9206..9fecc61b 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -83,7 +83,10 @@ namespace Kyoo .ConfigureLogging((context, builder) => { builder.AddConfiguration(context.Configuration.GetSection("logging")) - .AddConsole() + .AddSimpleConsole(x => + { + x.TimestampFormat = "[hh:mm:ss] "; + }) .AddDebug() .AddEventSourceLogger(); }) diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 0054c066..a612f7d5 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -47,7 +47,8 @@ namespace Kyoo _configuration = configuration; _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); - _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), new AuthenticationModule(configuration, loggerFactory)}); + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), + new AuthenticationModule(configuration, loggerFactory, host)}); } /// diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 32ea147c..e6a6cebf 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -73,7 +73,7 @@ namespace Kyoo.Tasks ICollection libraries = argument == null ? await libraryManager.GetAll() - : new [] { await libraryManager.Get(argument)}; + : new [] { await libraryManager.GetOrDefault(argument)}; if (argument != null && libraries.First() == null) throw new ArgumentException($"No library found with the name {argument}"); @@ -253,7 +253,7 @@ namespace Kyoo.Tasks { if (string.IsNullOrEmpty(collectionName)) return null; - Collection collection = await libraryManager.Get(Utility.ToSlug(collectionName)); + Collection collection = await libraryManager.GetOrDefault(Utility.ToSlug(collectionName)); if (collection != null) return collection; collection = await MetadataProvider.GetCollectionFromName(collectionName, library); @@ -265,7 +265,7 @@ namespace Kyoo.Tasks } catch (DuplicatedItemException) { - return await libraryManager.Get(collection.Slug); + return await libraryManager.GetOrDefault(collection.Slug); } } @@ -275,7 +275,7 @@ namespace Kyoo.Tasks bool isMovie, Library library) { - Show old = await libraryManager.Get(x => x.Path == showPath); + Show old = await libraryManager.GetOrDefault(x => x.Path == showPath); if (old != null) { await libraryManager.Load(old, x => x.ExternalIDs); @@ -291,7 +291,7 @@ namespace Kyoo.Tasks } catch (DuplicatedItemException) { - old = await libraryManager.Get(show.Slug); + old = await libraryManager.GetOrDefault(show.Slug); if (old.Path == showPath) { await libraryManager.Load(old, x => x.ExternalIDs); @@ -320,8 +320,15 @@ namespace Kyoo.Tasks catch (ItemNotFoundException) { Season season = await MetadataProvider.GetSeason(show, seasonNumber, library); - await libraryManager.CreateIfNotExists(season); - await ThumbnailsManager.Validate(season); + try + { + await libraryManager.Create(season); + await ThumbnailsManager.Validate(season); + } + catch (DuplicatedItemException) + { + season = await libraryManager.Get(show.Slug, seasonNumber); + } season.Show = show; return season; } diff --git a/Kyoo/Views/CollectionApi.cs b/Kyoo/Views/CollectionApi.cs index 0d5b0be5..92bcc406 100644 --- a/Kyoo/Views/CollectionApi.cs +++ b/Kyoo/Views/CollectionApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } @@ -92,7 +92,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index a77f408e..4935326a 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -50,14 +50,20 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task> GetShow(int showID, int seasonNumber, int episodeNumber) { - return await _libraryManager.Get(showID); + Show ret = await _libraryManager.GetOrDefault(showID); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{episodeID:int}/season")] [Authorize(Policy = "Read")] public async Task> GetSeason(int episodeID) { - return await _libraryManager.Get(x => x.Episodes.Any(y => y.ID == episodeID)); + Season ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] @@ -104,7 +110,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(episodeID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(episodeID) == null) return NotFound(); return Page(resources, limit); } @@ -175,7 +181,6 @@ namespace Kyoo.Api } [HttpGet("{id:int}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(int id) { try @@ -190,7 +195,6 @@ namespace Kyoo.Api } [HttpGet("{slug}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(string slug) { try diff --git a/Kyoo/Views/GenreApi.cs b/Kyoo/Views/GenreApi.cs index ab40f20b..c2df6b13 100644 --- a/Kyoo/Views/GenreApi.cs +++ b/Kyoo/Views/GenreApi.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -41,7 +40,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/LibraryApi.cs b/Kyoo/Views/LibraryApi.cs index 4ec44ea1..46707eec 100644 --- a/Kyoo/Views/LibraryApi.cs +++ b/Kyoo/Views/LibraryApi.cs @@ -6,7 +6,6 @@ using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Kyoo.CommonApi; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; @@ -27,7 +26,7 @@ namespace Kyoo.Api _taskManager = taskManager; } - [Authorize(Policy = "Admin")] + [Authorize(Policy = "Write")] public override async Task> Create(Library resource) { ActionResult result = await base.Create(resource); @@ -52,7 +51,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } @@ -104,7 +103,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } @@ -156,7 +155,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/PeopleApi.cs b/Kyoo/Views/PeopleApi.cs index ed68dea3..001caa33 100644 --- a/Kyoo/Views/PeopleApi.cs +++ b/Kyoo/Views/PeopleApi.cs @@ -87,18 +87,20 @@ namespace Kyoo.Api } [HttpGet("{id:int}/poster")] - [Authorize(Policy="Read")] public async Task GetPeopleIcon(int id) { - People people = await _libraryManager.Get(id); + People people = await _libraryManager.GetOrDefault(id); + if (people == null) + return NotFound(); return _files.FileResult(await _thumbs.GetPeoplePoster(people)); } [HttpGet("{slug}/poster")] - [Authorize(Policy="Read")] public async Task GetPeopleIcon(string slug) { - People people = await _libraryManager.Get(slug); + People people = await _libraryManager.GetOrDefault(slug); + if (people == null) + return NotFound(); return _files.FileResult(await _thumbs.GetPeoplePoster(people)); } } diff --git a/Kyoo/Views/ProviderApi.cs b/Kyoo/Views/ProviderApi.cs index 050f2681..20d08f7b 100644 --- a/Kyoo/Views/ProviderApi.cs +++ b/Kyoo/Views/ProviderApi.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -29,18 +28,20 @@ namespace Kyoo.Api } [HttpGet("{id:int}/logo")] - [Authorize(Policy="Read")] public async Task GetLogo(int id) { - Provider provider = await _libraryManager.Get(id); + Provider provider = await _libraryManager.GetOrDefault(id); + if (provider == null) + return NotFound(); return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); } [HttpGet("{slug}/logo")] - [Authorize(Policy="Read")] public async Task GetLogo(string slug) { - Provider provider = await _libraryManager.Get(slug); + Provider provider = await _libraryManager.GetOrDefault(slug); + if (provider == null) + return NotFound(); return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); } } diff --git a/Kyoo/Views/SeasonApi.cs b/Kyoo/Views/SeasonApi.cs index 9803f956..b578f350 100644 --- a/Kyoo/Views/SeasonApi.cs +++ b/Kyoo/Views/SeasonApi.cs @@ -47,7 +47,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(seasonID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(seasonID) == null) return NotFound(); return Page(resources, limit); } @@ -130,23 +130,28 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task> GetShow(int showID, int seasonNumber) { - return await _libraryManager.Get(showID); + Show ret = await _libraryManager.GetOrDefault(showID); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{id:int}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(int id) { - Season season = await _libraryManager.Get(id); + Season season = await _libraryManager.GetOrDefault(id); + if (season == null) + return NotFound(); await _libraryManager.Load(season, x => x.Show); return _files.FileResult(await _thumbs.GetSeasonPoster(season)); } [HttpGet("{slug}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(string slug) { - Season season = await _libraryManager.Get(slug); + Season season = await _libraryManager.GetOrDefault(slug); + if (season == null) + return NotFound(); await _libraryManager.Load(season, x => x.Show); return _files.FileResult(await _thumbs.GetSeasonPoster(season)); } diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index 7b2a8868..b1d17538 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -49,7 +49,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -75,7 +75,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -101,7 +101,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -127,7 +127,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -152,7 +152,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -177,7 +177,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -203,7 +203,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -229,7 +229,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -283,7 +283,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -309,7 +309,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -335,7 +335,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(showID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(showID) == null) return NotFound(); return Page(resources, limit); } @@ -361,7 +361,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -408,7 +408,6 @@ namespace Kyoo.Api } [HttpGet("{slug}/poster")] - [Authorize(Policy = "Read")] public async Task GetPoster(string slug) { try @@ -423,7 +422,6 @@ namespace Kyoo.Api } [HttpGet("{slug}/logo")] - [Authorize(Policy="Read")] public async Task GetLogo(string slug) { try @@ -438,7 +436,6 @@ namespace Kyoo.Api } [HttpGet("{slug}/backdrop")] - [Authorize(Policy="Read")] public async Task GetBackdrop(string slug) { try diff --git a/Kyoo/Views/StudioApi.cs b/Kyoo/Views/StudioApi.cs index 45f46829..ddf86092 100644 --- a/Kyoo/Views/StudioApi.cs +++ b/Kyoo/Views/StudioApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(id) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) return NotFound(); return Page(resources, limit); } @@ -66,7 +66,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index cc213cc0..ff3fffd3 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -20,12 +20,9 @@ "default": "Trace", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore.DbUpdateException": "None", - "Microsoft.EntityFrameworkCore.Update": "None", - "Microsoft.EntityFrameworkCore.Database.Command": "None", + "Microsoft.EntityFrameworkCore": "None", "Kyoo": "Trace" - }, - "dotnet-ef": "false" + } }, "authentication": { @@ -35,7 +32,7 @@ "password": "passphrase" }, "permissions": { - "default": ["read", "play", "write", "admin"], + "default": [], "newUser": ["read", "play", "write", "admin"] }, "profilePicturePath": "users/" From 440e5f4f141ac090b06e0c19cf707f9a82ae809e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 May 2021 15:58:55 +0200 Subject: [PATCH 20/25] Handling autologin and logout --- Kyoo.Authentication/AuthenticationModule.cs | 9 +++- Kyoo.Authentication/Extensions.cs | 15 ++++--- Kyoo.Authentication/IdentityDatabase.cs | 47 -------------------- Kyoo.Authentication/User.cs | 22 ---------- Kyoo.Authentication/Views/AccountApi.cs | 48 ++++++++++----------- Kyoo.Common/Controllers/IFileManager.cs | 3 +- Kyoo.WebApp | 2 +- 7 files changed, 42 insertions(+), 104 deletions(-) delete mode 100644 Kyoo.Authentication/IdentityDatabase.cs delete mode 100644 Kyoo.Authentication/User.cs diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 26b50dd1..e0bd5628 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -86,6 +86,12 @@ namespace Kyoo.Authentication IdentityModelEventSource.ShowPII = true; services.AddControllers(); + + // TODO handle direct-videos with bearers (probably add a ?token query param and a app.Use to translate that for videos) + + // TODO Support sign-out, check if login work, check if tokens should be stored. + + // TODO remove unused/commented code, add documentation. // services.AddIdentityCore() // .AddSignInManager() @@ -157,8 +163,7 @@ namespace Kyoo.Authentication services.AddAuthorization(options => { - AuthorizationPolicyBuilder scheme = new(IdentityConstants.ApplicationScheme, - JwtBearerDefaults.AuthenticationScheme); + AuthorizationPolicyBuilder scheme = new(JwtBearerDefaults.AuthenticationScheme); options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build(); string[] permissions = {"Read", "Write", "Play", "Admin"}; diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs index 4cac9dc8..5c65fade 100644 --- a/Kyoo.Authentication/Extensions.cs +++ b/Kyoo.Authentication/Extensions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Security.Claims; using IdentityModel; +using IdentityServer4; using Kyoo.Models; namespace Kyoo.Authentication @@ -24,16 +25,18 @@ namespace Kyoo.Authentication new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") }; } - + /// - /// Convert a user to a ClaimsPrincipal. + /// Convert a user to an . /// /// The user to convert - /// A ClaimsPrincipal representing the user - public static ClaimsPrincipal ToPrincipal(this User user) + /// The corresponding identity server user. + public static IdentityServerUser ToIdentityUser(this User user) { - ClaimsIdentity id = new (user.GetClaims()); - return new ClaimsPrincipal(id); + return new(user.ID.ToString()) + { + DisplayName = user.Username + }; } } } \ No newline at end of file diff --git a/Kyoo.Authentication/IdentityDatabase.cs b/Kyoo.Authentication/IdentityDatabase.cs deleted file mode 100644 index 8b4f321e..00000000 --- a/Kyoo.Authentication/IdentityDatabase.cs +++ /dev/null @@ -1,47 +0,0 @@ -// using System.Threading.Tasks; -// using IdentityServer4.EntityFramework.Entities; -// using IdentityServer4.EntityFramework.Extensions; -// using IdentityServer4.EntityFramework.Interfaces; -// using IdentityServer4.EntityFramework.Options; -// using Kyoo.Models; -// using Microsoft.AspNetCore.Identity; -// using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -// using Microsoft.EntityFrameworkCore; -// using Microsoft.Extensions.Options; -// -// namespace Kyoo -// { -// // The configuration's database is named ConfigurationDbContext. -// public class IdentityDatabase : IdentityDbContext, IPersistedGrantDbContext -// { -// private readonly IOptions _operationalStoreOptions; -// -// public IdentityDatabase(DbContextOptions options, IOptions operationalStoreOptions) -// : base(options) -// { -// _operationalStoreOptions = operationalStoreOptions; -// } -// -// public DbSet Accounts { get; set; } -// -// protected override void OnModelCreating(ModelBuilder modelBuilder) -// { -// base.OnModelCreating(modelBuilder); -// modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value); -// -// modelBuilder.Entity().ToTable("User"); -// modelBuilder.Entity>().ToTable("UserRole"); -// modelBuilder.Entity>().ToTable("UserLogin"); -// modelBuilder.Entity>().ToTable("UserClaim"); -// modelBuilder.Entity().ToTable("UserRoles"); -// modelBuilder.Entity>().ToTable("UserRoleClaim"); -// modelBuilder.Entity>().ToTable("UserToken"); -// } -// -// public Task SaveChangesAsync() => base.SaveChangesAsync(); -// -// public DbSet PersistedGrants { get; set; } -// public DbSet DeviceFlowCodes { get; set; } -// -// } -// } \ No newline at end of file diff --git a/Kyoo.Authentication/User.cs b/Kyoo.Authentication/User.cs deleted file mode 100644 index 896d3004..00000000 --- a/Kyoo.Authentication/User.cs +++ /dev/null @@ -1,22 +0,0 @@ -// using System; -// using IdentityModel; -// using Microsoft.AspNetCore.Identity; -// -// namespace Kyoo.Models -// { -// public class User : IdentityUser -// { -// public string OTAC { get; set; } -// public DateTime? OTACExpires { get; set; } -// -// public string GenerateOTAC(TimeSpan validFor) -// { -// string otac = CryptoRandom.CreateUniqueId(); -// string hashed = otac; // TODO should add a good hashing here. -// -// OTAC = hashed; -// OTACExpires = DateTime.UtcNow.Add(validFor); -// return otac; -// } -// } -// } \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 81bc3a55..d15ececc 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using IdentityServer4; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; @@ -35,15 +34,9 @@ namespace Kyoo.Authentication.Views /// private readonly IUserRepository _users; /// - /// The identity server interaction service to login users. - /// - // private readonly IIdentityServerInteractionService _interaction; - /// /// A file manager to send profile pictures /// private readonly IFileManager _files; - // private readonly SignInManager _signInManager; - /// /// Options about authentication. Those options are monitored and reloads are supported. /// @@ -54,20 +47,15 @@ namespace Kyoo.Authentication.Views /// Create a new handle to handle login/users requests. /// /// The user repository to create and manage users - /// The identity server interaction service to login users. /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, - // IIdentityServerInteractionService interaction, IFileManager files, IOptions options) - //, SignInManager signInManager) { _users = users; - // _interaction = interaction; _files = files; _options = options; - // _signInManager = signInManager; } @@ -119,7 +107,6 @@ namespace Kyoo.Authentication.Views [HttpPost("login")] public async Task Login([FromBody] LoginRequest login) { - // AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); User user = await _users.GetOrDefault(x => x.Username == login.Username); if (user == null) @@ -127,7 +114,7 @@ namespace Kyoo.Authentication.Views if (!PasswordUtils.CheckPassword(login.Password, user.Password)) return Unauthorized(); - // await _signInManager.SignInAsync(user, login.StayLoggedIn); + await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(login.StayLoggedIn)); return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true }); } @@ -143,20 +130,16 @@ namespace Kyoo.Authentication.Views User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac); if (user == null) return Unauthorized(); - if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow) + if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= + DateTime.UtcNow) + { return BadRequest(new { code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password." }); + } - - IdentityServerUser iduser = new(user.ID.ToString()) - { - DisplayName = user.Username - }; - - await HttpContext.SignInAsync(iduser, StayLogged(otac.StayLoggedIn)); - // await _signInManager.SignInAsync(user, otac.StayLoggedIn); + await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(otac.StayLoggedIn)); return Ok(); } @@ -167,11 +150,11 @@ namespace Kyoo.Authentication.Views [Authorize] public async Task Logout() { - // await _signInManager.SignOutAsync(); + await HttpContext.SignOutAsync(); return Ok(); } - // TODO check with the extension method + /// public async Task GetProfileDataAsync(ProfileDataRequestContext context) { User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); @@ -181,12 +164,18 @@ namespace Kyoo.Authentication.Views context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions))); } + /// public async Task IsActiveAsync(IsActiveContext context) { User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); context.IsActive = user != null; } + /// + /// Get the user's profile picture. + /// + /// The user slug + /// The profile picture of the user or 404 if not found [HttpGet("picture/{slug}")] public async Task GetPicture(string slug) { @@ -197,6 +186,11 @@ namespace Kyoo.Authentication.Views return _files.FileResult(path); } + /// + /// Update profile information (email, username, profile picture...) + /// + /// The new information + /// The edited user [HttpPut] [Authorize] public async Task> Update([FromForm] AccountUpdateRequest data) @@ -218,6 +212,10 @@ namespace Kyoo.Authentication.Views return await _users.Edit(user, false); } + /// + /// Get permissions for a non connected user. + /// + /// The list of permissions of a default user. [HttpGet("permissions")] public ActionResult> GetDefaultPermissions() { diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs index 0765c1d0..03f22e79 100644 --- a/Kyoo.Common/Controllers/IFileManager.cs +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -19,7 +19,8 @@ namespace Kyoo.Controllers /// or proxy them from a distant server /// /// - /// If no file exists at the given path, you should return a NotFoundResult or handle it gracefully. + /// If no file exists at the given path or if the path is null, a NotFoundResult is returned + /// to handle it gracefully. /// /// The path of the file. /// diff --git a/Kyoo.WebApp b/Kyoo.WebApp index d3a860fa..a0f8fe4d 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit d3a860fa8ffccade9e3b17022482e11c9a18303e +Subproject commit a0f8fe4de48a0f0770646d6052a09c551b6442dd From 765bd061b7764c6328e74f7852b763122955c8ee Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 May 2021 16:23:21 +0200 Subject: [PATCH 21/25] Cleaning up the Authentication plugin --- Kyoo.Authentication/AuthenticationModule.cs | 49 +------ .../{ => Controllers}/Certificates.cs | 0 Kyoo.Authentication/Controllers/UserStore.cs | 133 ------------------ .../{ => Models}/IdentityContext.cs | 22 +++ Kyoo/settings.json | 3 +- 5 files changed, 29 insertions(+), 178 deletions(-) rename Kyoo.Authentication/{ => Controllers}/Certificates.cs (100%) delete mode 100644 Kyoo.Authentication/Controllers/UserStore.cs rename Kyoo.Authentication/{ => Models}/IdentityContext.cs (69%) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index e0bd5628..b21214ff 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -89,28 +88,10 @@ namespace Kyoo.Authentication // TODO handle direct-videos with bearers (probably add a ?token query param and a app.Use to translate that for videos) - // TODO Support sign-out, check if login work, check if tokens should be stored. + // TODO Check if tokens should be stored. // TODO remove unused/commented code, add documentation. - // services.AddIdentityCore() - // .AddSignInManager() - // .AddDefaultTokenProviders() - // .AddUserStore(); - - // services.AddDbContext(options => - // { - // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); - // }); - - // services.AddIdentityCore(o => - // { - // o.Stores.MaxLengthForKeys = 128; - // }) - // .AddSignInManager() - // .AddDefaultTokenProviders() - // .AddEntityFrameworkStores(); - services.Configure(_configuration.GetSection(PermissionOption.Path)); services.Configure(_configuration.GetSection(CertificateOption.Path)); services.Configure(_configuration.GetSection(AuthenticationOption.Path)); @@ -124,35 +105,15 @@ namespace Kyoo.Authentication options.UserInteraction.ErrorUrl = $"{publicUrl}/error"; options.UserInteraction.LogoutUrl = $"{publicUrl}/logout"; }) - // .AddAspNetIdentity() - // .AddConfigurationStore(options => - // { - // options.ConfigureDbContext = builder => - // builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), - // sql => sql.MigrationsAssembly(assemblyName)); - // }) - // .AddOperationalStore(options => - // { - // options.ConfigureDbContext = builder => - // builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"), - // sql => sql.MigrationsAssembly(assemblyName)); - // options.EnableTokenCleanup = true; - // }) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) .AddInMemoryClients(IdentityContext.GetClients()) + .AddInMemoryClients(_configuration.GetSection("authentication:clients")) .AddProfileService() .AddSigninKeys(certificateOptions); - // TODO implement means to add clients or api scopes for other plugins. // TODO split scopes (kyoo.read should be task.read, video.read etc) - - // services.AddAuthentication(o => - // { - // o.DefaultScheme = IdentityConstants.ApplicationScheme; - // o.DefaultSignInScheme = IdentityConstants.ExternalScheme; - // }) - // .AddIdentityCookies(_ => { }); + services.AddAuthentication() .AddJwtBearer(options => { @@ -171,10 +132,10 @@ namespace Kyoo.Authentication { options.AddPolicy(permission, policy => { - // policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme); policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); policy.AddRequirements(new AuthRequirement(permission)); - // policy.RequireScope($"kyoo.{permission.ToLower()}"); + // Scopes are disables to support default permissions. + // To enable them, use the following line: policy.RequireScope($"kyoo.{permission.ToLower()}"); }); } }); diff --git a/Kyoo.Authentication/Certificates.cs b/Kyoo.Authentication/Controllers/Certificates.cs similarity index 100% rename from Kyoo.Authentication/Certificates.cs rename to Kyoo.Authentication/Controllers/Certificates.cs diff --git a/Kyoo.Authentication/Controllers/UserStore.cs b/Kyoo.Authentication/Controllers/UserStore.cs deleted file mode 100644 index 0649796f..00000000 --- a/Kyoo.Authentication/Controllers/UserStore.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Identity; - -namespace Kyoo.Authentication -{ - /// - /// An implementation of an that uses an . - /// - public class UserStore : IUserStore - { - /// - /// The user repository used to store users. - /// - private readonly IUserRepository _users; - - /// - /// Create a new . - /// - /// The user repository to use - public UserStore(IUserRepository users) - { - _users = users; - } - - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Implementation of the IDisposable pattern - /// - /// True if this class should be disposed. - protected virtual void Dispose(bool disposing) - { - bool _ = disposing; - // Not implemented because this class has nothing to dispose. - } - - /// - public Task GetUserIdAsync(User user, CancellationToken cancellationToken) - { - return Task.FromResult(user.ID.ToString()); - } - - /// - public Task GetUserNameAsync(User user, CancellationToken cancellationToken) - { - return Task.FromResult(user.Username); - } - - /// - public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken) - { - user.Username = userName; - return Task.CompletedTask; - } - - /// - public Task GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken) - { - return Task.FromResult(user.Slug); - } - - /// - public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken) - { - user.Slug = normalizedName; - return Task.CompletedTask; - } - - /// - public async Task CreateAsync(User user, CancellationToken cancellationToken) - { - try - { - await _users.Create(user); - return IdentityResult.Success; - } - catch (Exception ex) - { - return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); - } - } - - /// - public async Task UpdateAsync(User user, CancellationToken cancellationToken) - { - try - { - await _users.Edit(user, false); - return IdentityResult.Success; - } - catch (Exception ex) - { - return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); - } - } - - /// - public async Task DeleteAsync(User user, CancellationToken cancellationToken) - { - try - { - await _users.Delete(user); - return IdentityResult.Success; - } - catch (Exception ex) - { - return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message}); - } - } - - /// - public Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - return _users.GetOrDefault(int.Parse(userId)); - } - - /// - public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - return _users.GetOrDefault(normalizedUserName); - } - } -} \ No newline at end of file diff --git a/Kyoo.Authentication/IdentityContext.cs b/Kyoo.Authentication/Models/IdentityContext.cs similarity index 69% rename from Kyoo.Authentication/IdentityContext.cs rename to Kyoo.Authentication/Models/IdentityContext.cs index 071a601b..e6ca3353 100644 --- a/Kyoo.Authentication/IdentityContext.cs +++ b/Kyoo.Authentication/Models/IdentityContext.cs @@ -4,8 +4,15 @@ using IdentityServer4.Models; namespace Kyoo.Authentication { + /// + /// The hard coded context of the identity server. + /// public static class IdentityContext { + /// + /// The list of identity resources supported (email, profile and openid) + /// + /// The list of identity resources supported public static IEnumerable GetIdentityResources() { return new List @@ -16,6 +23,13 @@ namespace Kyoo.Authentication }; } + /// + /// The list of officially supported clients. + /// + /// + /// You can add custom clients in the settings.json file. + /// + /// The list of officially supported clients. public static IEnumerable GetClients() { return new List @@ -40,6 +54,10 @@ namespace Kyoo.Authentication }; } + /// + /// The list of scopes supported by the API. + /// + /// The list of scopes public static IEnumerable GetScopes() { return new[] @@ -67,6 +85,10 @@ namespace Kyoo.Authentication }; } + /// + /// The list of APIs (this is used to create Audiences) + /// + /// The list of apis public static IEnumerable GetApis() { return new[] diff --git a/Kyoo/settings.json b/Kyoo/settings.json index ff3fffd3..34b24da9 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -35,7 +35,8 @@ "default": [], "newUser": ["read", "play", "write", "admin"] }, - "profilePicturePath": "users/" + "profilePicturePath": "users/", + "clients": [] }, From 71c18092e57c0d03b2952fb45c392e749daa8d9c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 11 May 2021 00:13:37 +0200 Subject: [PATCH 22/25] Reworking authorization handling --- Kyoo.Authentication/AuthenticationModule.cs | 39 ++--- .../AuthorizationValidatorHandler.cs | 51 ------ .../Controllers/PremissionValidator.cs | 126 +++++++++++++++ Kyoo.Authentication/Extensions.cs | 14 +- Kyoo.Authentication/Models/AuthRequirement.cs | 24 --- Kyoo.Authentication/Views/AccountApi.cs | 2 +- .../Models/Attributes/PermissionAttribute.cs | 148 ++++++++++++++++-- Kyoo.CommonAPI/CrudApi.cs | 24 +-- Kyoo/Startup.cs | 3 +- Kyoo/Views/CollectionApi.cs | 15 +- Kyoo/Views/EpisodeApi.cs | 31 ++-- Kyoo/Views/GenreApi.cs | 9 +- Kyoo/Views/LibraryApi.cs | 23 +-- Kyoo/Views/LibraryItemApi.cs | 4 +- Kyoo/Views/PeopleApi.cs | 7 +- Kyoo/Views/ProviderApi.cs | 6 +- Kyoo/Views/SearchApi.cs | 21 ++- Kyoo/Views/SeasonApi.cs | 25 +-- Kyoo/Views/ShowApi.cs | 35 +++-- Kyoo/Views/StudioApi.cs | 7 +- Kyoo/Views/SubtitleApi.cs | 6 +- Kyoo/Views/TaskApi.cs | 8 +- Kyoo/Views/TrackApi.cs | 7 +- Kyoo/Views/VideoApi.cs | 12 +- Kyoo/Views/WatchApi.cs | 4 +- Kyoo/settings.json | 4 +- 26 files changed, 427 insertions(+), 228 deletions(-) delete mode 100644 Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs create mode 100644 Kyoo.Authentication/Controllers/PremissionValidator.cs delete mode 100644 Kyoo.Authentication/Models/AuthRequirement.cs diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index b21214ff..9018ba79 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using IdentityServer4.Extensions; +using IdentityServer4.Models; using IdentityServer4.Services; using Kyoo.Authentication.Models; using Kyoo.Authentication.Views; using Kyoo.Controllers; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -86,18 +87,20 @@ namespace Kyoo.Authentication services.AddControllers(); - // TODO handle direct-videos with bearers (probably add a ?token query param and a app.Use to translate that for videos) + // TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos) // TODO Check if tokens should be stored. - // TODO remove unused/commented code, add documentation. - services.Configure(_configuration.GetSection(PermissionOption.Path)); services.Configure(_configuration.GetSection(CertificateOption.Path)); services.Configure(_configuration.GetSection(AuthenticationOption.Path)); + + + List clients = new(); + _configuration.GetSection("authentication:clients").Bind(clients); CertificateOption certificateOptions = new(); _configuration.GetSection(CertificateOption.Path).Bind(certificateOptions); - + services.AddIdentityServer(options => { options.IssuerUri = publicUrl; @@ -108,11 +111,9 @@ namespace Kyoo.Authentication .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) - .AddInMemoryClients(IdentityContext.GetClients()) - .AddInMemoryClients(_configuration.GetSection("authentication:clients")) + .AddInMemoryClients(IdentityContext.GetClients().Concat(clients)) .AddProfileService() .AddSigninKeys(certificateOptions); - // TODO split scopes (kyoo.read should be task.read, video.read etc) services.AddAuthentication() .AddJwtBearer(options => @@ -121,25 +122,7 @@ namespace Kyoo.Authentication options.Audience = "kyoo"; options.RequireHttpsMetadata = false; }); - - services.AddAuthorization(options => - { - AuthorizationPolicyBuilder scheme = new(JwtBearerDefaults.AuthenticationScheme); - options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build(); - - string[] permissions = {"Read", "Write", "Play", "Admin"}; - foreach (string permission in permissions) - { - options.AddPolicy(permission, policy => - { - policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); - policy.AddRequirements(new AuthRequirement(permission)); - // Scopes are disables to support default permissions. - // To enable them, use the following line: policy.RequireScope($"kyoo.{permission.ToLower()}"); - }); - } - }); - services.AddSingleton(); + services.AddSingleton(); DefaultCorsPolicyService cors = new(_loggerFactory.CreateLogger()) { diff --git a/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs b/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs deleted file mode 100644 index 1df1c86a..00000000 --- a/Kyoo.Authentication/Controllers/AuthorizationValidatorHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using IdentityServer4.Extensions; -using Kyoo.Authentication.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Options; - -namespace Kyoo.Authentication -{ - /// - /// The default IAuthorizationHandler implementation. - /// - public class AuthorizationValidatorHandler : AuthorizationHandler - { - /// - /// The permissions options to retrieve default permissions. - /// - private readonly IOptionsMonitor _options; - - /// - /// Create a new . - /// - /// The option containing default values. - public AuthorizationValidatorHandler(IOptionsMonitor options) - { - _options = options; - } - - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthRequirement requirement) - { - if (context.User.IsAuthenticated()) - { - Claim perms = context.User.Claims.FirstOrDefault(x => x.Type == "permissions"); - if (perms != null && perms.Value.Split(",").Contains(requirement.Permission.ToLower())) - context.Succeed(requirement); - } - else - { - ICollection defaultPerms = _options.CurrentValue.Default; - if (defaultPerms?.Contains(requirement.Permission.ToLower()) == true) - context.Succeed(requirement); - } - - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Kyoo.Authentication/Controllers/PremissionValidator.cs b/Kyoo.Authentication/Controllers/PremissionValidator.cs new file mode 100644 index 00000000..04f13ada --- /dev/null +++ b/Kyoo.Authentication/Controllers/PremissionValidator.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Authentication.Models; +using Kyoo.Models.Permissions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; + +namespace Kyoo.Authentication +{ + /// + /// A permission validator to validate permission with user Permission array + /// or the default array from the configurations if the user is not logged. + /// + public class PermissionValidatorFactory : IPermissionValidator + { + /// + /// The permissions options to retrieve default permissions. + /// + private readonly IOptionsMonitor _options; + + /// + /// Create a new factory with the given options + /// + /// The option containing default values. + public PermissionValidatorFactory(IOptionsMonitor options) + { + _options = options; + } + + /// + public IFilterMetadata Create(PermissionAttribute attribute) + { + return new PermissionValidator(attribute.AsPermissionString(), _options); + } + + /// + public IFilterMetadata Create(PartialPermissionAttribute attribute) + { + return new PermissionValidator((object)attribute.Type ?? attribute.Kind, _options); + } + + /// + /// The authorization filter used by + /// + private class PermissionValidator : IAsyncAuthorizationFilter + { + /// + /// The permission to validate + /// + private readonly string _permission; + /// + /// Information about partial items. + /// + private readonly object _partialInfo; + /// + /// The permissions options to retrieve default permissions. + /// + private readonly IOptionsMonitor _options; + + /// + /// Create a new permission validator with the given options + /// + /// The permission to validate + /// The option containing default values. + public PermissionValidator(string permission, IOptionsMonitor options) + { + _permission = permission; + _options = options; + } + + /// + /// Create a new permission validator with the given options + /// + /// The partial permission to validate + /// The option containing default values. + public PermissionValidator(object partialInfo, IOptionsMonitor options) + { + _partialInfo = partialInfo; + _options = options; + } + + + /// + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + string permission = _permission; + + if (_partialInfo != null) + { + switch (context.HttpContext.Items["PermissionType"]) + { + case string perm when _partialInfo is Kind kind: + permission = $"{perm}.{kind.ToString().ToLower()}"; + break; + case Kind kind when _partialInfo is string partial: + permission = $"{partial}.{kind.ToString().ToLower()}"; + break; + case null: + context.HttpContext.Items["PermissionType"] = _partialInfo; + return; + default: + throw new ArgumentException("Multiple non-matching partial permission attribute " + + "are not supported."); + } + } + + AuthenticateResult res = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + if (res.Succeeded) + { + if (res.Principal.GetPermissions().All(x => x != permission)) + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); + } + else + { + if (res.Failure != null || _options.CurrentValue.Default.All(x => x != permission)) + context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized); + } + } + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs index 5c65fade..718a7a44 100644 --- a/Kyoo.Authentication/Extensions.cs +++ b/Kyoo.Authentication/Extensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using IdentityModel; using IdentityServer4; @@ -35,8 +36,19 @@ namespace Kyoo.Authentication { return new(user.ID.ToString()) { - DisplayName = user.Username + DisplayName = user.Username, + AdditionalClaims = new[] {new Claim("permissions", string.Join(',', user.Permissions))} }; } + + /// + /// Get the permissions of an user. + /// + /// The user + /// The list of permissions + public static ICollection GetPermissions(this ClaimsPrincipal user) + { + return user.Claims.FirstOrDefault(x => x.Type == "permissions")?.Value.Split(','); + } } } \ No newline at end of file diff --git a/Kyoo.Authentication/Models/AuthRequirement.cs b/Kyoo.Authentication/Models/AuthRequirement.cs deleted file mode 100644 index b74818b5..00000000 --- a/Kyoo.Authentication/Models/AuthRequirement.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Authentication -{ - /// - /// The requirement of Kyoo's authentication policies. - /// - public class AuthRequirement : IAuthorizationRequirement - { - /// - /// The name of the permission - /// - public string Permission { get; } - - /// - /// Create a new for the given permission. - /// - /// The permission needed - public AuthRequirement(string permission) - { - Permission = permission; - } - } -} \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index d15ececc..59d964f0 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -13,8 +13,8 @@ using Kyoo.Authentication.Models.DTO; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/Kyoo.Common/Models/Attributes/PermissionAttribute.cs b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs index fd2285e9..b18ce8ee 100644 --- a/Kyoo.Common/Models/Attributes/PermissionAttribute.cs +++ b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs @@ -1,23 +1,149 @@ using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; -namespace Kyoo.Models.Attributes +namespace Kyoo.Models.Permissions { + /// + /// The kind of permission needed. + /// + public enum Kind + { + Read, + Write, + Create, + Delete + } + /// /// Specify permissions needed for the API. /// - [AttributeUsage(AttributeTargets.Method)] - public class PermissionAttribute : Attribute + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class PermissionAttribute : Attribute, IFilterFactory { - public enum Kind - { - Read, - Write, - Admin - } - + /// + /// The needed permission as string. + /// + private readonly string _permission; + + /// + /// Ask a permission to run an action. + /// + /// + /// The type of the action + /// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)). + /// + /// The kind of permission needed public PermissionAttribute(string type, Kind permission) { - + if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase)) + type = type[..^3]; + _permission = $"{type.ToLower()}.{permission.ToString().ToLower()}"; + } + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService().Create(this); + } + + /// + public bool IsReusable => true; + + /// + /// Return this permission attribute as a string + /// + /// The string representation. + public string AsPermissionString() + { + return _permission; } } + + /// + /// Specify one part of a permissions needed for the API (the kind or the type). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class PartialPermissionAttribute : Attribute, IFilterFactory + { + /// + /// The needed permission type. + /// + public string Type { get; } + /// + /// The needed permission kind. + /// + public Kind Kind { get; } + + /// + /// Ask a permission to run an action. + /// + /// + /// With this attribute, you can only specify a type or a kind. + /// To have a valid permission attribute, you must specify the kind and the permission using two attributes. + /// Those attributes can be dispatched at different places (one on the class, one on the method for example). + /// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will + /// lead to unspecified behaviors. + /// + /// + /// The type of the action + /// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)). + /// + public PartialPermissionAttribute(string type) + { + if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase)) + type = type[..^3]; + Type = type.ToLower(); + } + + /// + /// Ask a permission to run an action. + /// + /// + /// With this attribute, you can only specify a type or a kind. + /// To have a valid permission attribute, you must specify the kind and the permission using two attributes. + /// Those attributes can be dispatched at different places (one on the class, one on the method for example). + /// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will + /// lead to unspecified behaviors. + /// + /// The kind of permission needed + public PartialPermissionAttribute(Kind permission) + { + Kind = permission; + } + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService().Create(this); + } + + /// + public bool IsReusable => true; + } + + + /// + /// A service to validate permissions + /// + public interface IPermissionValidator + { + /// + /// Create an IAuthorizationFilter that will be used to validate permissions. + /// This can registered with any lifetime. + /// + /// The permission attribute to validate + /// An authorization filter used to validate the permission + IFilterMetadata Create(PermissionAttribute attribute); + + /// + /// Create an IAuthorizationFilter that will be used to validate permissions. + /// This can registered with any lifetime. + /// + /// + /// A partial attribute to validate. See . + /// + /// An authorization filter used to validate the permission + IFilterMetadata Create(PartialPermissionAttribute attribute); + } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index f14f10db..06713c66 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -26,7 +26,7 @@ namespace Kyoo.CommonApi [HttpGet("{id:int}")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> Get(int id) { T ret = await _repository.GetOrDefault(id); @@ -36,7 +36,7 @@ namespace Kyoo.CommonApi } [HttpGet("{slug}")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> Get(string slug) { T ret = await _repository.Get(slug); @@ -46,7 +46,7 @@ namespace Kyoo.CommonApi } [HttpGet("count")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> GetCount([FromQuery] Dictionary where) { try @@ -60,7 +60,7 @@ namespace Kyoo.CommonApi } [HttpGet] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task>> GetAll([FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, @@ -90,7 +90,7 @@ namespace Kyoo.CommonApi } [HttpPost] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Create)] public virtual async Task> Create([FromBody] T resource) { try @@ -109,7 +109,7 @@ namespace Kyoo.CommonApi } [HttpPut] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Write)] public virtual async Task> Edit([FromQuery] bool resetOld, [FromBody] T resource) { try @@ -128,7 +128,7 @@ namespace Kyoo.CommonApi } [HttpPut("{id:int}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Write)] public virtual async Task> Edit(int id, [FromQuery] bool resetOld, [FromBody] T resource) { resource.ID = id; @@ -143,7 +143,7 @@ namespace Kyoo.CommonApi } [HttpPut("{slug}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Write)] public virtual async Task> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) { try @@ -159,7 +159,7 @@ namespace Kyoo.CommonApi } [HttpDelete("{id:int}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(int id) { try @@ -175,7 +175,7 @@ namespace Kyoo.CommonApi } [HttpDelete("{slug}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(string slug) { try @@ -190,7 +190,7 @@ namespace Kyoo.CommonApi return Ok(); } - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(Dictionary where) { try diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index a612f7d5..a1bda566 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -7,6 +7,7 @@ using Kyoo.Postgresql; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; @@ -131,7 +132,7 @@ namespace Kyoo app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllers(); }); diff --git a/Kyoo/Views/CollectionApi.cs b/Kyoo/Views/CollectionApi.cs index 92bcc406..214cb60d 100644 --- a/Kyoo/Views/CollectionApi.cs +++ b/Kyoo/Views/CollectionApi.cs @@ -6,7 +6,7 @@ using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Kyoo.CommonApi; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -14,6 +14,7 @@ namespace Kyoo.Api [Route("api/collection")] [Route("api/collections")] [ApiController] + [PartialPermission(nameof(CollectionApi))] public class CollectionApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -26,7 +27,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/show")] [HttpGet("{id:int}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -52,7 +53,7 @@ namespace Kyoo.Api [HttpGet("{slug}/show")] [HttpGet("{slug}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -66,7 +67,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -78,7 +79,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/library")] [HttpGet("{id:int}/libraries")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetLibraries(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -104,7 +105,7 @@ namespace Kyoo.Api [HttpGet("{slug}/library")] [HttpGet("{slug}/libraries")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetLibraries(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -118,7 +119,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index 4935326a..c87a2704 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -15,6 +15,7 @@ namespace Kyoo.Api [Route("api/episode")] [Route("api/episodes")] [ApiController] + [PartialPermission(nameof(EpisodeApi))] public class EpisodeApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -33,21 +34,27 @@ namespace Kyoo.Api } [HttpGet("{episodeID:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(int episodeID) { - return await _libraryManager.Get(x => x.Episodes.Any(y => y.ID == episodeID)); + Show ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(string showSlug, int seasonNumber, int episodeNumber) { - return await _libraryManager.Get(showSlug); + Show ret = await _libraryManager.GetOrDefault(showSlug); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(int showID, int seasonNumber, int episodeNumber) { Show ret = await _libraryManager.GetOrDefault(showID); @@ -57,7 +64,7 @@ namespace Kyoo.Api } [HttpGet("{episodeID:int}/season")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetSeason(int episodeID) { Season ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); @@ -67,7 +74,7 @@ namespace Kyoo.Api } [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetSeason(string showSlug, int seasonNumber, int episodeNumber) { try @@ -81,7 +88,7 @@ namespace Kyoo.Api } [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetSeason(int showID, int seasonNumber, int episodeNumber) { try @@ -96,7 +103,7 @@ namespace Kyoo.Api [HttpGet("{episodeID:int}/track")] [HttpGet("{episodeID:int}/tracks")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(int episodeID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -122,7 +129,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/track")] [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(int showID, int seasonNumber, int episodeNumber, @@ -152,7 +159,7 @@ namespace Kyoo.Api [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/track")] [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(string slug, int seasonNumber, int episodeNumber, diff --git a/Kyoo/Views/GenreApi.cs b/Kyoo/Views/GenreApi.cs index c2df6b13..83ebb38d 100644 --- a/Kyoo/Views/GenreApi.cs +++ b/Kyoo/Views/GenreApi.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -14,6 +14,7 @@ namespace Kyoo.Api [Route("api/genre")] [Route("api/genres")] [ApiController] + [PartialPermission(nameof(GenreApi))] public class GenreApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -26,7 +27,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/show")] [HttpGet("{id:int}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -52,7 +53,7 @@ namespace Kyoo.Api [HttpGet("{slug}/show")] [HttpGet("{slug}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -66,7 +67,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/LibraryApi.cs b/Kyoo/Views/LibraryApi.cs index 46707eec..239dcd8e 100644 --- a/Kyoo/Views/LibraryApi.cs +++ b/Kyoo/Views/LibraryApi.cs @@ -6,7 +6,7 @@ using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Kyoo.CommonApi; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -14,6 +14,7 @@ namespace Kyoo.Api [Route("api/library")] [Route("api/libraries")] [ApiController] + [PartialPermission(nameof(LibraryAPI))] public class LibraryAPI : CrudApi { private readonly ILibraryManager _libraryManager; @@ -26,7 +27,7 @@ namespace Kyoo.Api _taskManager = taskManager; } - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Create)] public override async Task> Create(Library resource) { ActionResult result = await base.Create(resource); @@ -37,7 +38,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/show")] [HttpGet("{id:int}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -63,7 +64,7 @@ namespace Kyoo.Api [HttpGet("{slug}/show")] [HttpGet("{slug}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -77,7 +78,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -89,7 +90,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/collection")] [HttpGet("{id:int}/collections")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetCollections(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -115,7 +116,7 @@ namespace Kyoo.Api [HttpGet("{slug}/collection")] [HttpGet("{slug}/collections")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetCollections(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -129,7 +130,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } @@ -141,7 +142,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/item")] [HttpGet("{id:int}/items")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetItems(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -167,7 +168,7 @@ namespace Kyoo.Api [HttpGet("{slug}/item")] [HttpGet("{slug}/items")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetItems(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -181,7 +182,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.Get(slug) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/LibraryItemApi.cs b/Kyoo/Views/LibraryItemApi.cs index 5c36cbc2..08367985 100644 --- a/Kyoo/Views/LibraryItemApi.cs +++ b/Kyoo/Views/LibraryItemApi.cs @@ -6,7 +6,7 @@ using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -29,7 +29,7 @@ namespace Kyoo.Api } [HttpGet] - [Authorize(Policy = "Read")] + [Permission(nameof(LibraryItemApi), Kind.Read)] public async Task>> GetAll([FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, diff --git a/Kyoo/Views/PeopleApi.cs b/Kyoo/Views/PeopleApi.cs index 001caa33..724198e5 100644 --- a/Kyoo/Views/PeopleApi.cs +++ b/Kyoo/Views/PeopleApi.cs @@ -5,7 +5,7 @@ using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -13,6 +13,7 @@ namespace Kyoo.Api { [Route("api/people")] [ApiController] + [PartialPermission(nameof(PeopleApi))] public class PeopleApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -32,7 +33,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/role")] [HttpGet("{id:int}/roles")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetRoles(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -60,7 +61,7 @@ namespace Kyoo.Api [HttpGet("{slug}/role")] [HttpGet("{slug}/roles")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetRoles(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, diff --git a/Kyoo/Views/ProviderApi.cs b/Kyoo/Views/ProviderApi.cs index 20d08f7b..133c15fd 100644 --- a/Kyoo/Views/ProviderApi.cs +++ b/Kyoo/Views/ProviderApi.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -10,13 +11,14 @@ namespace Kyoo.Api [Route("api/provider")] [Route("api/providers")] [ApiController] - public class ProviderAPI : CrudApi + [PartialPermission(nameof(ProviderApi))] + public class ProviderApi : CrudApi { private readonly IThumbnailsManager _thumbnails; private readonly ILibraryManager _libraryManager; private readonly IFileManager _files; - public ProviderAPI(ILibraryManager libraryManager, + public ProviderApi(ILibraryManager libraryManager, IConfiguration config, IFileManager files, IThumbnailsManager thumbnails) diff --git a/Kyoo/Views/SearchApi.cs b/Kyoo/Views/SearchApi.cs index 008bf0f5..b78aa683 100644 --- a/Kyoo/Views/SearchApi.cs +++ b/Kyoo/Views/SearchApi.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; namespace Kyoo.Api @@ -19,7 +19,12 @@ namespace Kyoo.Api } [HttpGet] - [Authorize(Policy="Read")] + [Permission(nameof(Collection), Kind.Read)] + [Permission(nameof(Show), Kind.Read)] + [Permission(nameof(Episode), Kind.Read)] + [Permission(nameof(People), Kind.Read)] + [Permission(nameof(Genre), Kind.Read)] + [Permission(nameof(Studio), Kind.Read)] public async Task> Search(string query) { return new SearchResult @@ -36,7 +41,7 @@ namespace Kyoo.Api [HttpGet("collection")] [HttpGet("collections")] - [Authorize(Policy="Read")] + [Permission(nameof(Collection), Kind.Read)] public Task> SearchCollections(string query) { return _libraryManager.Search(query); @@ -44,7 +49,7 @@ namespace Kyoo.Api [HttpGet("show")] [HttpGet("shows")] - [Authorize(Policy="Read")] + [Permission(nameof(Show), Kind.Read)] public Task> SearchShows(string query) { return _libraryManager.Search(query); @@ -52,14 +57,14 @@ namespace Kyoo.Api [HttpGet("episode")] [HttpGet("episodes")] - [Authorize(Policy="Read")] + [Permission(nameof(Episode), Kind.Read)] public Task> SearchEpisodes(string query) { return _libraryManager.Search(query); } [HttpGet("people")] - [Authorize(Policy="Read")] + [Permission(nameof(People), Kind.Read)] public Task> SearchPeople(string query) { return _libraryManager.Search(query); @@ -67,7 +72,7 @@ namespace Kyoo.Api [HttpGet("genre")] [HttpGet("genres")] - [Authorize(Policy="Read")] + [Permission(nameof(Genre), Kind.Read)] public Task> SearchGenres(string query) { return _libraryManager.Search(query); @@ -75,7 +80,7 @@ namespace Kyoo.Api [HttpGet("studio")] [HttpGet("studios")] - [Authorize(Policy="Read")] + [Permission(nameof(Studio), Kind.Read)] public Task> SearchStudios(string query) { return _libraryManager.Search(query); diff --git a/Kyoo/Views/SeasonApi.cs b/Kyoo/Views/SeasonApi.cs index b578f350..1987e7c5 100644 --- a/Kyoo/Views/SeasonApi.cs +++ b/Kyoo/Views/SeasonApi.cs @@ -4,9 +4,9 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Linq; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -14,6 +14,7 @@ namespace Kyoo.Api [Route("api/season")] [Route("api/seasons")] [ApiController] + [PartialPermission(nameof(SeasonApi))] public class SeasonApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -33,7 +34,7 @@ namespace Kyoo.Api [HttpGet("{seasonID:int}/episode")] [HttpGet("{seasonID:int}/episodes")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(int seasonID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -59,7 +60,7 @@ namespace Kyoo.Api [HttpGet("{showSlug}-s{seasonNumber:int}/episode")] [HttpGet("{showSlug}-s{seasonNumber:int}/episodes")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(string showSlug, int seasonNumber, [FromQuery] string sortBy, @@ -87,7 +88,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}-s{seasonNumber:int}/episode")] [HttpGet("{showID:int}-s{seasonNumber:int}/episodes")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisode(int showID, int seasonNumber, [FromQuery] string sortBy, @@ -113,21 +114,27 @@ namespace Kyoo.Api } [HttpGet("{seasonID:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(int seasonID) { - return await _libraryManager.Get(x => x.Seasons.Any(y => y.ID == seasonID)); + Show ret = await _libraryManager.GetOrDefault(x => x.Seasons.Any(y => y.ID == seasonID)); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{showSlug}-s{seasonNumber:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(string showSlug, int seasonNumber) { - return await _libraryManager.Get(showSlug); + Show ret = await _libraryManager.GetOrDefault(showSlug); + if (ret == null) + return NotFound(); + return ret; } [HttpGet("{showID:int}-s{seasonNumber:int}/show")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetShow(int showID, int seasonNumber) { Show ret = await _libraryManager.GetOrDefault(showID); diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index b1d17538..8b73afd2 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -16,6 +16,7 @@ namespace Kyoo.Api [Route("api/show")] [Route("api/shows")] [ApiController] + [PartialPermission(nameof(ShowApi))] public class ShowApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -35,7 +36,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}/season")] [HttpGet("{showID:int}/seasons")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetSeasons(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -61,7 +62,7 @@ namespace Kyoo.Api [HttpGet("{slug}/season")] [HttpGet("{slug}/seasons")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetSeasons(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -87,7 +88,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}/episode")] [HttpGet("{showID:int}/episodes")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisodes(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -113,7 +114,7 @@ namespace Kyoo.Api [HttpGet("{slug}/episode")] [HttpGet("{slug}/episodes")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetEpisodes(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -138,7 +139,7 @@ namespace Kyoo.Api } [HttpGet("{showID:int}/people")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetPeople(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -163,7 +164,7 @@ namespace Kyoo.Api } [HttpGet("{slug}/people")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetPeople(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -189,7 +190,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}/genre")] [HttpGet("{showID:int}/genres")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetGenres(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -215,7 +216,7 @@ namespace Kyoo.Api [HttpGet("{slug}/genre")] [HttpGet("{slug}/genres")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetGenre(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -240,7 +241,7 @@ namespace Kyoo.Api } [HttpGet("{showID:int}/studio")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetStudio(int showID) { try @@ -254,7 +255,7 @@ namespace Kyoo.Api } [HttpGet("{slug}/studio")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetStudio(string slug) { try @@ -269,7 +270,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}/library")] [HttpGet("{showID:int}/libraries")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetLibraries(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -295,7 +296,7 @@ namespace Kyoo.Api [HttpGet("{slug}/library")] [HttpGet("{slug}/libraries")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetLibraries(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -321,7 +322,7 @@ namespace Kyoo.Api [HttpGet("{showID:int}/collection")] [HttpGet("{showID:int}/collections")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetCollections(int showID, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -347,7 +348,7 @@ namespace Kyoo.Api [HttpGet("{slug}/collection")] [HttpGet("{slug}/collections")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetCollections(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -373,7 +374,7 @@ namespace Kyoo.Api [HttpGet("{slug}/font")] [HttpGet("{slug}/fonts")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetFonts(string slug) { try @@ -392,7 +393,7 @@ namespace Kyoo.Api [HttpGet("{showSlug}/font/{slug}")] [HttpGet("{showSlug}/fonts/{slug}")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task GetFont(string showSlug, string slug) { try diff --git a/Kyoo/Views/StudioApi.cs b/Kyoo/Views/StudioApi.cs index ddf86092..66b6ca5d 100644 --- a/Kyoo/Views/StudioApi.cs +++ b/Kyoo/Views/StudioApi.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -14,6 +14,7 @@ namespace Kyoo.Api [Route("api/studio")] [Route("api/studios")] [ApiController] + [PartialPermission(nameof(ShowApi))] public class StudioAPI : CrudApi { private readonly ILibraryManager _libraryManager; @@ -26,7 +27,7 @@ namespace Kyoo.Api [HttpGet("{id:int}/show")] [HttpGet("{id:int}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(int id, [FromQuery] string sortBy, [FromQuery] int afterID, @@ -52,7 +53,7 @@ namespace Kyoo.Api [HttpGet("{slug}/show")] [HttpGet("{slug}/shows")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task>> GetShows(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 7052d9eb..5e680a73 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; namespace Kyoo.Api { @@ -23,8 +23,8 @@ namespace Kyoo.Api } - [HttpGet("{slug}.{extension?}")] - [Authorize(Policy="Play")] + [HttpGet("{slug}.{extension}")] + [Permission(nameof(SubtitleApi), Kind.Read)] public async Task GetSubtitle(string slug, string extension) { Track subtitle; diff --git a/Kyoo/Views/TaskApi.cs b/Kyoo/Views/TaskApi.cs index 91aa2a5d..b75033d7 100644 --- a/Kyoo/Views/TaskApi.cs +++ b/Kyoo/Views/TaskApi.cs @@ -2,17 +2,14 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Kyoo.Controllers; -using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; -using static Kyoo.Models.Attributes.PermissionAttribute; +using Kyoo.Models.Permissions; namespace Kyoo.Api { [Route("api/task")] [Route("api/tasks")] [ApiController] - // [Authorize(Policy="Admin")] public class TaskApi : ControllerBase { private readonly ITaskManager _taskManager; @@ -24,7 +21,7 @@ namespace Kyoo.Api [HttpGet] - [Permission("task", Kind.Read)] + [Permission(nameof(TaskApi), Kind.Read)] public ActionResult> GetTasks() { return Ok(_taskManager.GetAllTasks()); @@ -32,6 +29,7 @@ namespace Kyoo.Api [HttpGet("{taskSlug}")] [HttpPut("{taskSlug}")] + [Permission(nameof(TaskApi), Kind.Create)] public IActionResult RunTask(string taskSlug, [FromQuery] Dictionary args) { try diff --git a/Kyoo/Views/TrackApi.cs b/Kyoo/Views/TrackApi.cs index 77125bdc..77f8669e 100644 --- a/Kyoo/Views/TrackApi.cs +++ b/Kyoo/Views/TrackApi.cs @@ -4,7 +4,7 @@ using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -13,6 +13,7 @@ namespace Kyoo.Api [Route("api/track")] [Route("api/tracks")] [ApiController] + [PartialPermission(nameof(Track))] public class TrackApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -24,7 +25,7 @@ namespace Kyoo.Api } [HttpGet("{id:int}/episode")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetEpisode(int id) { try @@ -38,7 +39,7 @@ namespace Kyoo.Api } [HttpGet("{slug}/episode")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetEpisode(string slug) { try diff --git a/Kyoo/Views/VideoApi.cs b/Kyoo/Views/VideoApi.cs index 498a93a2..050b82b4 100644 --- a/Kyoo/Views/VideoApi.cs +++ b/Kyoo/Views/VideoApi.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using System.Threading.Tasks; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc.Filters; namespace Kyoo.Api @@ -44,7 +44,7 @@ namespace Kyoo.Api [HttpGet("{slug}")] [HttpGet("direct/{slug}")] - [Authorize(Policy="Play")] + // TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)] public async Task Direct(string slug) { try @@ -59,7 +59,7 @@ namespace Kyoo.Api } [HttpGet("transmux/{slug}/master.m3u8")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public async Task Transmux(string slug) { try @@ -78,7 +78,7 @@ namespace Kyoo.Api } [HttpGet("transcode/{slug}/master.m3u8")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public async Task Transcode(string slug) { try @@ -98,7 +98,7 @@ namespace Kyoo.Api [HttpGet("transmux/{episodeLink}/segments/{chunk}")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public IActionResult GetTransmuxedChunk(string episodeLink, string chunk) { string path = Path.GetFullPath(Path.Combine(_transmuxPath, episodeLink)); @@ -107,7 +107,7 @@ namespace Kyoo.Api } [HttpGet("transcode/{episodeLink}/segments/{chunk}")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public IActionResult GetTranscodedChunk(string episodeLink, string chunk) { string path = Path.GetFullPath(Path.Combine(_transcodePath, episodeLink)); diff --git a/Kyoo/Views/WatchApi.cs b/Kyoo/Views/WatchApi.cs index 7418a6ae..38e65e21 100644 --- a/Kyoo/Views/WatchApi.cs +++ b/Kyoo/Views/WatchApi.cs @@ -2,7 +2,7 @@ using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.AspNetCore.Mvc; namespace Kyoo.Api @@ -19,7 +19,7 @@ namespace Kyoo.Api } [HttpGet("{slug}")] - [Authorize(Policy="Read")] + [Permission("video", Kind.Read)] public async Task> GetWatchItem(string slug) { try diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 34b24da9..60213e60 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -32,8 +32,8 @@ "password": "passphrase" }, "permissions": { - "default": [], - "newUser": ["read", "play", "write", "admin"] + "default": ["read", "play", "write", "admin"], + "newUser": ["read", "play", "write", "admin", "task.read"] }, "profilePicturePath": "users/", "clients": [] From 21e354bf007789951d675efc723f990536f44c5b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 11 May 2021 01:20:25 +0200 Subject: [PATCH 23/25] Handling overall permissions --- .../Controllers/PremissionValidator.cs | 47 +++++++++++++------ .../Models/Attributes/PermissionAttribute.cs | 11 +++-- .../PassthroughPermissionValidator.cs | 35 ++++++++++++++ Kyoo/CoreModule.cs | 5 ++ Kyoo/Kyoo.csproj | 2 +- Kyoo/Startup.cs | 5 +- Kyoo/settings.json | 4 +- 7 files changed, 85 insertions(+), 24 deletions(-) create mode 100644 Kyoo/Controllers/PassthroughPermissionValidator.cs diff --git a/Kyoo.Authentication/Controllers/PremissionValidator.cs b/Kyoo.Authentication/Controllers/PremissionValidator.cs index 04f13ada..dc60faa7 100644 --- a/Kyoo.Authentication/Controllers/PremissionValidator.cs +++ b/Kyoo.Authentication/Controllers/PremissionValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Kyoo.Authentication.Models; @@ -35,7 +36,7 @@ namespace Kyoo.Authentication /// public IFilterMetadata Create(PermissionAttribute attribute) { - return new PermissionValidator(attribute.AsPermissionString(), _options); + return new PermissionValidator(attribute.Type, attribute.Kind, _options); } /// @@ -54,9 +55,9 @@ namespace Kyoo.Authentication /// private readonly string _permission; /// - /// Information about partial items. + /// The kind of permission needed /// - private readonly object _partialInfo; + private readonly Kind? _kind; /// /// The permissions options to retrieve default permissions. /// @@ -66,10 +67,12 @@ namespace Kyoo.Authentication /// Create a new permission validator with the given options /// /// The permission to validate + /// The kind of permission needed /// The option containing default values. - public PermissionValidator(string permission, IOptionsMonitor options) + public PermissionValidator(string permission, Kind kind, IOptionsMonitor options) { _permission = permission; + _kind = kind; _options = options; } @@ -80,7 +83,12 @@ namespace Kyoo.Authentication /// The option containing default values. public PermissionValidator(object partialInfo, IOptionsMonitor options) { - _partialInfo = partialInfo; + if (partialInfo is Kind kind) + _kind = kind; + else if (partialInfo is string perm) + _permission = perm; + else + throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind."); _options = options; } @@ -89,35 +97,46 @@ namespace Kyoo.Authentication public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { string permission = _permission; + Kind? kind = _kind; - if (_partialInfo != null) + if (permission == null || kind == null) { switch (context.HttpContext.Items["PermissionType"]) { - case string perm when _partialInfo is Kind kind: - permission = $"{perm}.{kind.ToString().ToLower()}"; + case string perm: + permission = perm; break; - case Kind kind when _partialInfo is string partial: - permission = $"{partial}.{kind.ToString().ToLower()}"; + case Kind kin: + kind = kin; break; - case null: - context.HttpContext.Items["PermissionType"] = _partialInfo; + case null when kind != null: + context.HttpContext.Items["PermissionType"] = kind; + return; + case null when permission != null: + context.HttpContext.Items["PermissionType"] = permission; return; default: throw new ArgumentException("Multiple non-matching partial permission attribute " + "are not supported."); } + if (permission == null || kind == null) + throw new ArgumentException("The permission type or kind is still missing after two partial " + + "permission attributes, this is unsupported."); } + string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}"; + string overallStr = $"overall.{kind.ToString()!.ToLower()}"; AuthenticateResult res = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); if (res.Succeeded) { - if (res.Principal.GetPermissions().All(x => x != permission)) + ICollection permissions = res.Principal.GetPermissions(); + if (permissions.All(x => x != permStr && x != overallStr)) context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); } else { - if (res.Failure != null || _options.CurrentValue.Default.All(x => x != permission)) + ICollection permissions = _options.CurrentValue.Default ?? Array.Empty(); + if (res.Failure != null || permissions.All(x => x != permStr && x != overallStr)) context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized); } } diff --git a/Kyoo.Common/Models/Attributes/PermissionAttribute.cs b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs index b18ce8ee..b34fb48b 100644 --- a/Kyoo.Common/Models/Attributes/PermissionAttribute.cs +++ b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs @@ -24,7 +24,11 @@ namespace Kyoo.Models.Permissions /// /// The needed permission as string. /// - private readonly string _permission; + public string Type { get; } + /// + /// The needed permission kind. + /// + public Kind Kind { get; } /// /// Ask a permission to run an action. @@ -38,7 +42,8 @@ namespace Kyoo.Models.Permissions { if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase)) type = type[..^3]; - _permission = $"{type.ToLower()}.{permission.ToString().ToLower()}"; + Type = type.ToLower(); + Kind = permission; } /// @@ -56,7 +61,7 @@ namespace Kyoo.Models.Permissions /// The string representation. public string AsPermissionString() { - return _permission; + return Type; } } diff --git a/Kyoo/Controllers/PassthroughPermissionValidator.cs b/Kyoo/Controllers/PassthroughPermissionValidator.cs new file mode 100644 index 00000000..d6d2f334 --- /dev/null +++ b/Kyoo/Controllers/PassthroughPermissionValidator.cs @@ -0,0 +1,35 @@ +using Kyoo.Models.Permissions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace Kyoo.Controllers +{ + /// + /// A permission validator that always validate permissions. This effectively disable the permission system. + /// + public class PassthroughPermissionValidator : IPermissionValidator + { + // ReSharper disable once SuggestBaseTypeForParameter + public PassthroughPermissionValidator(ILogger logger) + { + logger.LogWarning("No permission validator has been enabled, all users will have all permissions"); + } + + /// + public IFilterMetadata Create(PermissionAttribute attribute) + { + return new PassthroughValidator(); + } + + /// + public IFilterMetadata Create(PartialPermissionAttribute attribute) + { + return new PassthroughValidator(); + } + + /// + /// An useless filter that does nothing. + /// + private class PassthroughValidator : IFilterMetadata { } + } +} \ No newline at end of file diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index c2d5e321..3111177e 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using Kyoo.Controllers; +using Kyoo.Models.Permissions; using Kyoo.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -93,6 +95,9 @@ namespace Kyoo } services.AddTask(); + + if (services.All(x => x.ServiceType != typeof(IPermissionValidator))) + services.AddSingleton(); } } } \ No newline at end of file diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 7c39d8d3..5804420f 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -45,7 +45,7 @@ - + all diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index a1bda566..062f62f5 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,13 +1,11 @@ using System; using System.IO; -using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Postgresql; using Kyoo.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; @@ -48,8 +46,7 @@ namespace Kyoo _configuration = configuration; _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); - _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), - new AuthenticationModule(configuration, loggerFactory, host)}); + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); } /// diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 60213e60..4e575d0b 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -32,8 +32,8 @@ "password": "passphrase" }, "permissions": { - "default": ["read", "play", "write", "admin"], - "newUser": ["read", "play", "write", "admin", "task.read"] + "default": ["overall.read", "overall.write", "overall.create", "overall.delete"], + "newUser": ["overall.read", "overall.write", "overall.create", "overall.delete"] }, "profilePicturePath": "users/", "clients": [] From 23e44d5efeed00069889e4b056a574919222fa34 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 13 May 2021 21:04:38 +0200 Subject: [PATCH 24/25] Moving login files to the authentication module --- Kyoo.Authentication/AuthenticationModule.cs | 23 ++++++++++++++++-- .../Kyoo.Authentication.csproj | 24 ++++++++++++++++--- Kyoo.CommonAPI/CrudApi.cs | 2 +- Kyoo.Postgresql/Kyoo.Postgresql.csproj | 10 ++++++-- Kyoo/Controllers/PluginManager.cs | 1 + Kyoo/Kyoo.csproj | 12 ++-------- Kyoo/Startup.cs | 10 +++++--- Kyoo/Views/LibraryItemApi.cs | 2 +- Kyoo/settings.json | 2 +- 9 files changed, 63 insertions(+), 23 deletions(-) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 9018ba79..9c7c3687 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Services; @@ -13,9 +15,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Logging; +using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode; namespace Kyoo.Authentication { @@ -80,7 +84,7 @@ namespace Kyoo.Authentication /// public void Configure(IServiceCollection services, ICollection availableTypes) { - string publicUrl = _configuration.GetValue("public_url").TrimEnd('/'); + string publicUrl = _configuration.GetValue("publicUrl").TrimEnd('/'); if (_environment.IsDevelopment()) IdentityModelEventSource.ShowPII = true; @@ -141,11 +145,26 @@ namespace Kyoo.Authentication app.UseAuthentication(); app.Use((ctx, next) => { - ctx.SetIdentityServerOrigin(_configuration.GetValue("public_url")); + ctx.SetIdentityServerOrigin(_configuration.GetValue("publicUrl").TrimEnd('/')); return next(); }); app.UseIdentityServer(); app.UseAuthorization(); + + PhysicalFileProvider provider = new(Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + "login")); + app.UseDefaultFiles(new DefaultFilesOptions + { + RequestPath = new PathString("/login"), + FileProvider = provider, + RedirectToAppendTrailingSlash = true + }); + app.UseStaticFiles(new StaticFileOptions + { + RequestPath = new PathString("/login"), + FileProvider = provider + }); } } } \ No newline at end of file diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj index b334a943..2cde6c7e 100644 --- a/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -13,6 +13,7 @@ Zoe Roux https://github.com/AnonymusRaccoon/Kyoo default + ../Kyoo.WebLogin/ @@ -21,12 +22,29 @@ - - - + all false + runtime + + + + + + + + + login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) + PreserveNewest + true + + + + + + + diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 06713c66..75a2758c 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -21,7 +21,7 @@ namespace Kyoo.CommonApi public CrudApi(IRepository repository, IConfiguration configuration) { _repository = repository; - BaseURL = configuration.GetValue("public_url").TrimEnd('/'); + BaseURL = configuration.GetValue("publicUrl").TrimEnd('/'); } diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index c9067984..52c9041b 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -27,9 +27,15 @@ - - + + all + false + runtime + + all + false + runtime diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index c17e3230..321d2f57 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -218,6 +218,7 @@ namespace Kyoo.Controllers }); if (existing != null) return existing; + // TODO load the assembly from the common folder if the file exists (this would allow shared libraries) string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) return LoadFromAssemblyPath(assemblyPath); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5804420f..77c01a03 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -6,7 +6,6 @@ Latest false ../Kyoo.WebApp/ - ../Kyoo.WebLogin/ ../Kyoo.Transcoder/ $(DefaultItemExcludes);$(SpaRoot)node_modules/** @@ -52,7 +51,6 @@ - @@ -72,18 +70,13 @@ - + wwwroot/%(StaticFiles.RecursiveDir)%(StaticFiles.Filename)%(StaticFiles.Extension) PreserveNewest true - - wwwroot/login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) - PreserveNewest - true - @@ -91,9 +84,8 @@ - + - diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 062f62f5..e30f495e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Postgresql; @@ -46,7 +47,11 @@ namespace Kyoo _configuration = configuration; _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); - _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); + // TODO remove postgres from here and load it like a normal plugin. + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), + new PostgresModule(configuration, host), + new AuthenticationModule(configuration, loggerFactory, host) + }); } /// @@ -55,7 +60,7 @@ namespace Kyoo /// The service collection to fill. public void ConfigureServices(IServiceCollection services) { - string publicUrl = _configuration.GetValue("public_url"); + string publicUrl = _configuration.GetValue("publicUrl"); services.AddMvc().AddControllersAsServices(); @@ -100,7 +105,6 @@ namespace Kyoo FileExtensionContentTypeProvider contentTypeProvider = new(); contentTypeProvider.Mappings[".data"] = "application/octet-stream"; - app.UseDefaultFiles(); app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider, diff --git a/Kyoo/Views/LibraryItemApi.cs b/Kyoo/Views/LibraryItemApi.cs index 08367985..6cd81b05 100644 --- a/Kyoo/Views/LibraryItemApi.cs +++ b/Kyoo/Views/LibraryItemApi.cs @@ -25,7 +25,7 @@ namespace Kyoo.Api public LibraryItemApi(ILibraryItemRepository libraryItems, IConfiguration configuration) { _libraryItems = libraryItems; - _baseURL = configuration.GetValue("public_url").TrimEnd('/'); + _baseURL = configuration.GetValue("publicUrl").TrimEnd('/'); } [HttpGet] diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 4e575d0b..e8d72e8f 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -1,6 +1,6 @@ { "server.urls": "http://*:5000", - "public_url": "http://localhost:5000/", + "publicUrl": "http://localhost:5000/", "database": { "postgres": { From 8fcbcd125f81ed6bf97d7bac6b83e65d6770825a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 13 May 2021 21:27:12 +0200 Subject: [PATCH 25/25] Fixing authentication issues in publish mode --- Kyoo.WebApp | 2 +- Kyoo/Kyoo.csproj | 27 +++++++++++++++++++++++++-- Kyoo/settings.json | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Kyoo.WebApp b/Kyoo.WebApp index a0f8fe4d..22a02671 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit a0f8fe4de48a0f0770646d6052a09c551b6442dd +Subproject commit 22a02671918201d6d9d4e80a76f01b59b216a82d diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 77c01a03..22fd6e21 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -39,12 +39,14 @@ - + + + - all + @@ -104,4 +106,25 @@ false + + + + + ../Kyoo.WebLogin/ + + + + + + + + login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) + PreserveNewest + true + + + + + + diff --git a/Kyoo/settings.json b/Kyoo/settings.json index e8d72e8f..fed2bd58 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -17,7 +17,7 @@ "logging": { "logLevel": { - "default": "Trace", + "default": "Warning", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "Microsoft.EntityFrameworkCore": "None",