diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs new file mode 100644 index 00000000..9c7c3687 --- /dev/null +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Kyoo.Authentication.Models; +using Kyoo.Authentication.Views; +using Kyoo.Controllers; +using Kyoo.Models.Permissions; +using Microsoft.AspNetCore.Builder; +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 +{ + /// + /// 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 => new [] + { + typeof(IUserRepository) + }; + + + /// + /// The configuration to use. + /// + private readonly IConfiguration _configuration; + + /// + /// A logger factory to allow IdentityServer to log things. + /// + 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 + /// 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; + } + + /// + public void Configure(IServiceCollection services, ICollection availableTypes) + { + string publicUrl = _configuration.GetValue("publicUrl").TrimEnd('/'); + + if (_environment.IsDevelopment()) + IdentityModelEventSource.ShowPII = true; + + services.AddControllers(); + + // 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. + + 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; + options.UserInteraction.LoginUrl = $"{publicUrl}/login"; + options.UserInteraction.ErrorUrl = $"{publicUrl}/error"; + options.UserInteraction.LogoutUrl = $"{publicUrl}/logout"; + }) + .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) + .AddInMemoryApiScopes(IdentityContext.GetScopes()) + .AddInMemoryApiResources(IdentityContext.GetApis()) + .AddInMemoryClients(IdentityContext.GetClients().Concat(clients)) + .AddProfileService() + .AddSigninKeys(certificateOptions); + + services.AddAuthentication() + .AddJwtBearer(options => + { + options.Authority = publicUrl; + options.Audience = "kyoo"; + options.RequireHttpsMetadata = false; + }); + services.AddSingleton(); + + DefaultCorsPolicyService cors = new(_loggerFactory.CreateLogger()) + { + AllowedOrigins = {new Uri(publicUrl).GetLeftPart(UriPartial.Authority)} + }; + services.AddSingleton(cors); + } + + /// + public void ConfigureAspNet(IApplicationBuilder app) + { + app.UseCookiePolicy(new CookiePolicyOptions + { + MinimumSameSitePolicy = SameSiteMode.Strict + }); + app.UseAuthentication(); + app.Use((ctx, next) => + { + 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/Controllers/Certificates.cs b/Kyoo.Authentication/Controllers/Certificates.cs new file mode 100644 index 00000000..3aaab9b8 --- /dev/null +++ b/Kyoo.Authentication/Controllers/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/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/Controllers/PremissionValidator.cs b/Kyoo.Authentication/Controllers/PremissionValidator.cs new file mode 100644 index 00000000..dc60faa7 --- /dev/null +++ b/Kyoo.Authentication/Controllers/PremissionValidator.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +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.Type, attribute.Kind, _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; + /// + /// The kind of permission needed + /// + private readonly Kind? _kind; + /// + /// 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 kind of permission needed + /// The option containing default values. + public PermissionValidator(string permission, Kind kind, IOptionsMonitor options) + { + _permission = permission; + _kind = kind; + _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) + { + 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; + } + + + /// + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + string permission = _permission; + Kind? kind = _kind; + + if (permission == null || kind == null) + { + switch (context.HttpContext.Items["PermissionType"]) + { + case string perm: + permission = perm; + break; + case Kind kin: + kind = kin; + break; + 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) + { + ICollection permissions = res.Principal.GetPermissions(); + if (permissions.All(x => x != permStr && x != overallStr)) + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); + } + else + { + ICollection permissions = _options.CurrentValue.Default ?? Array.Empty(); + if (res.Failure != null || permissions.All(x => x != permStr && x != overallStr)) + context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized); + } + } + } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs new file mode 100644 index 00000000..718a7a44 --- /dev/null +++ b/Kyoo.Authentication/Extensions.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using IdentityModel; +using IdentityServer4; +using Kyoo.Models; + +namespace Kyoo.Authentication +{ + /// + /// Extension methods. + /// + public static class Extensions + { + /// + /// Get claims of an user. + /// + /// The user concerned + /// The list of claims the user has + public static ICollection GetClaims(this User user) + { + return new[] + { + new Claim(JwtClaimTypes.Subject, user.ID.ToString()), + new Claim(JwtClaimTypes.Name, user.Username), + new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") + }; + } + + /// + /// Convert a user to an . + /// + /// The user to convert + /// The corresponding identity server user. + public static IdentityServerUser ToIdentityUser(this User user) + { + return new(user.ID.ToString()) + { + 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/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj new file mode 100644 index 00000000..2cde6c7e --- /dev/null +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -0,0 +1,50 @@ + + + + net5.0 + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/authentication + false + false + false + false + true + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + ../Kyoo.WebLogin/ + + + + + + + + + + + all + false + runtime + + + + + + + + + + + login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) + PreserveNewest + true + + + + + + + + diff --git a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs new file mode 100644 index 00000000..ac135799 --- /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(ErrorMessage = "The email is invalid.")] + public string Email { get; set; } + + /// + /// The new username of the user. + /// + [MinLength(4, ErrorMessage = "The username must have at least 4 characters")] + public string Username { get; set; } + + /// + /// The picture icon. + /// + public IFormFile Picture { get; set; } + } +} \ No newline at end of file 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/DTO/RegisterRequest.cs b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs new file mode 100644 index 00000000..ad556f6d --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +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(ErrorMessage = "The email must be a valid email address")] + public string Email { get; set; } + + /// + /// The user's username. + /// + [MinLength(4, ErrorMessage = "The username must have at least {1} characters")] + public string Username { get; set; } + + /// + /// The user's password. + /// + [MinLength(8, ErrorMessage = "The password must have at least {1} characters")] + 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, + ExtraData = new Dictionary() + }; + } + } +} \ No newline at end of file diff --git a/Kyoo/Models/IdentityContext.cs b/Kyoo.Authentication/Models/IdentityContext.cs similarity index 65% rename from Kyoo/Models/IdentityContext.cs rename to Kyoo.Authentication/Models/IdentityContext.cs index c414efcc..e6ca3353 100644 --- a/Kyoo/Models/IdentityContext.cs +++ b/Kyoo.Authentication/Models/IdentityContext.cs @@ -2,10 +2,17 @@ using System.Collections.Generic; using System.Linq; using IdentityServer4.Models; -namespace Kyoo +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 }; } + /// + /// 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 @@ -23,7 +37,7 @@ namespace Kyoo new() { ClientId = "kyoo.webapp", - + AccessTokenType = AccessTokenType.Jwt, AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, @@ -33,13 +47,17 @@ namespace Kyoo 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" } } }; } + /// + /// The list of scopes supported by the API. + /// + /// The list of scopes public static IEnumerable GetScopes() { return new[] @@ -60,11 +78,6 @@ namespace Kyoo 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." @@ -72,13 +85,16 @@ namespace Kyoo }; } + /// + /// The list of APIs (this is used to create Audiences) + /// + /// The list of apis public static IEnumerable GetApis() { return new[] { - new ApiResource + new ApiResource("kyoo", "Kyoo") { - Name = "Kyoo", Scopes = GetScopes().Select(x => x.Name).ToArray() } }; diff --git a/Kyoo.Authentication/Models/Options/AuthenticationOption.cs b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs new file mode 100644 index 00000000..23e917aa --- /dev/null +++ b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs @@ -0,0 +1,28 @@ +namespace Kyoo.Authentication.Models +{ + /// + /// The main authentication options. + /// + public class AuthenticationOption + { + /// + /// 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/Options/CertificateOption.cs b/Kyoo.Authentication/Models/Options/CertificateOption.cs new file mode 100644 index 00000000..93d2a878 --- /dev/null +++ b/Kyoo.Authentication/Models/Options/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.Authentication/Models/Options/PermissionOption.cs b/Kyoo.Authentication/Models/Options/PermissionOption.cs new file mode 100644 index 00000000..8d6c698d --- /dev/null +++ b/Kyoo.Authentication/Models/Options/PermissionOption.cs @@ -0,0 +1,23 @@ +namespace Kyoo.Authentication.Models +{ + /// + /// Permission options. + /// + public class PermissionOption + { + /// + /// The path to get this option from the root configuration. + /// + public const string Path = "authentication:permissions"; + + /// + /// The default permissions that will be given to a non-connected user. + /// + public string[] Default { get; set; } + + /// + /// Permissions applied to a new user. + /// + 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 new file mode 100644 index 00000000..59d964f0 --- /dev/null +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Claims; +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.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Kyoo.Authentication.Views +{ + /// + /// 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; + /// + /// A file manager to send profile pictures + /// + private readonly IFileManager _files; + /// + /// Options about authentication. Those options are monitored and reloads are supported. + /// + private readonly IOptions _options; + + + /// + /// Create a new handle to handle login/users requests. + /// + /// The user repository to create and manage users + /// A file manager to send profile pictures + /// Authentication options (this may be hot reloaded) + public AccountApi(IUserRepository users, + IFileManager files, + IOptions options) + { + _users = users; + _files = files; + _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.Value.Permissions.NewUser; + user.Password = PasswordUtils.HashPassword(user.Password); + user.ExtraData["otac"] = PasswordUtils.GenerateOTAC(); + user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s"); + 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"]}); + } + + /// + /// 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) + { + 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.ToIdentityUser(), 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) + { + // 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) + { + return BadRequest(new + { + code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password." + }); + } + + await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(otac.StayLoggedIn)); + return Ok(); + } + + /// + /// Sign out an user + /// + [HttpGet("logout")] + [Authorize] + public async Task Logout() + { + await HttpContext.SignOutAsync(); + return Ok(); + } + + /// + public async Task GetProfileDataAsync(ProfileDataRequestContext context) + { + 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.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) + { + User user = await _users.GetOrDefault(slug); + if (user == null) + return NotFound(); + string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); + 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) + { + 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)) + user.Username = data.Username; + if (data.Picture?.Length > 0) + { + string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); + await using Stream file = _files.NewFile(path); + await data.Picture.CopyToAsync(file); + } + 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() + { + return _options.Value.Permissions.Default ?? Array.Empty(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs index cc3c70bb..03f22e79 100644 --- a/Kyoo.Common/Controllers/IFileManager.cs +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -7,21 +7,88 @@ 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 or if the path is null, a NotFoundResult is returned + /// to 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/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index d15987de..2cd0c909 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,8 +82,9 @@ 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 + [ItemNotNull] Task Get(int id) where T : class, IResource; /// @@ -91,8 +92,9 @@ 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 + [ItemNotNull] Task Get(string slug) where T : class, IResource; /// @@ -100,8 +102,9 @@ 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 + [ItemNotNull] Task Get(Expression> where) where T : class, IResource; /// @@ -109,8 +112,9 @@ 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 + [ItemNotNull] Task Get(int showID, int seasonNumber); /// @@ -118,8 +122,9 @@ 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 + [ItemNotNull] Task Get(string showSlug, int seasonNumber); /// @@ -128,8 +133,9 @@ 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 + [ItemNotNull] Task Get(int showID, int seasonNumber, int episodeNumber); /// @@ -138,8 +144,9 @@ 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 + [ItemNotNull] Task Get(string showSlug, int seasonNumber, int episodeNumber); /// @@ -147,8 +154,9 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The tracl found + /// If the item is not 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); @@ -505,7 +521,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 +530,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 +538,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 +546,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 new file mode 100644 index 00000000..3201df83 --- /dev/null +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +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 + { + /// + /// 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 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. + /// + ICollection Provides { get; } + + /// + /// 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). + /// + ICollection Requires { get; } + + /// + /// A configure method that will be run on plugin's startup. + /// + /// 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 > + /// 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 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. + 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) {} + } + + /// + /// 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.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs index 643cce7a..bd4ef513 100644 --- a/Kyoo.Common/Controllers/IPluginManager.cs +++ b/Kyoo.Common/Controllers/IPluginManager.cs @@ -1,13 +1,56 @@ using System.Collections.Generic; -using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; 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(); - public void ReloadPlugins(); + + /// + /// 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 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); + + /// + /// Configure an asp net application applying plugins policies. + /// + /// The asp net application to configure + public void ConfigureAspnet(IApplicationBuilder app); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 2ed0b19a..dad2d5e3 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; } @@ -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); @@ -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,86 +205,85 @@ 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. /// 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 /// - /// 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) + /// If the item is not found + /// The resource edited and completed by database's information (related items & so on) Task Edit([NotNull] T edited, bool resetOld); /// /// 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 + /// 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 - /// If the item is not found + /// 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 - /// If the item is not found + /// 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 - /// If the item is not found + /// An enumerable of resource's slug + /// 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); } @@ -294,7 +293,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 @@ -306,7 +305,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 +320,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 +329,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 +361,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 +370,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 +396,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 +404,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,8 +419,8 @@ namespace Kyoo.Controllers /// /// The slug of the track /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The tracl found + /// If the item is not found + /// The track found Task Get(string slug, StreamType type = StreamType.Unknown); /// @@ -429,7 +428,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 +438,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 +455,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 +469,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 +481,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 +520,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 +546,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 +572,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 +598,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 +630,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 +641,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/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs new file mode 100644 index 00000000..75277dd2 --- /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 run 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 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 , + /// 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..392355d3 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -1,13 +1,34 @@ +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 = null); + + /// + /// Get all currently running tasks + /// + /// A list of currently running tasks. + ICollection GetRunningTasks(); + + /// + /// Get all available tasks + /// + /// A list of every tasks that this instance know. + ICollection GetAllTasks(); } } \ No newline at end of file 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..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 ItemNotFound(); + throw new ItemNotFoundException($"No repository found for the type {typeof(T).Name}."); } /// diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 21d71761..349ef6a0 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -21,8 +21,10 @@ - + + + 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/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs new file mode 100644 index 00000000..1e9a8ece --- /dev/null +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -0,0 +1,16 @@ +using System; +using JetBrains.Annotations; +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)] + [MeansImplicitUse(ImplicitUseKindFlags.Assign)] + public class InjectedAttribute : Attribute { } +} \ 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..b34fb48b --- /dev/null +++ b/Kyoo.Common/Models/Attributes/PermissionAttribute.cs @@ -0,0 +1,154 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Models.Permissions +{ + /// + /// The kind of permission needed. + /// + public enum Kind + { + Read, + Write, + Create, + Delete + } + + /// + /// Specify permissions needed for the API. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class PermissionAttribute : Attribute, IFilterFactory + { + /// + /// The needed permission as string. + /// + public string Type { get; } + /// + /// The needed permission kind. + /// + public Kind Kind { get; } + + /// + /// 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]; + Type = type.ToLower(); + Kind = permission; + } + + /// + 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 Type; + } + } + + /// + /// 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.Common/Models/Exceptions/DuplicatedItemException.cs b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs index 19be8a04..b0d26bf0 100644 --- a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs +++ b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs @@ -1,19 +1,36 @@ using System; +using System.Runtime.Serialization; 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) + { } + + /// + /// 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/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..d05882b1 --- /dev/null +++ b/Kyoo.Common/Models/Exceptions/ItemNotFoundException.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +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) + { } + + /// + /// 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/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/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/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/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.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs new file mode 100644 index 00000000..94afa240 --- /dev/null +++ b/Kyoo.Common/Models/Resources/User.cs @@ -0,0 +1,69 @@ +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 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. + /// + 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 CurrentlyWatching { get; set; } + +#if ENABLE_INTERNAL_LINKS + /// + /// Links between Users and Shows. + /// + public ICollection> ShowLinks { 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/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..c1c09165 --- /dev/null +++ b/Kyoo.Common/Module.cs @@ -0,0 +1,66 @@ +using System; +using Kyoo.Controllers; +using Microsoft.Extensions.DependencyInjection; + +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 IServiceCollection AddTask(this IServiceCollection services) + where T : class, ITask + { + services.AddSingleton(); + return services; + } + + /// + /// Register a new repository to 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 + /// + /// The initial 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) + 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 + /// + /// The initial container. + public static IServiceCollection AddRepository(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where T2 : IBaseRepository, T + { + services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime)); + return services.AddRepository(lifetime); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 220b2003..6583f704 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); @@ -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 @@ -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) { @@ -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) @@ -712,70 +712,18 @@ namespace Kyoo }, TaskContinuationOptions.ExecuteSynchronously); } - public static Expression> ResourceEquals(IResource obj) - where T : IResource + /// + /// 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 (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; + 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/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 50dcb588..75a2758c 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; @@ -21,40 +21,32 @@ namespace Kyoo.CommonApi public CrudApi(IRepository repository, IConfiguration configuration) { _repository = repository; - BaseURL = configuration.GetValue("public_url").TrimEnd('/'); + BaseURL = configuration.GetValue("publicUrl").TrimEnd('/'); } [HttpGet("{id:int}")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> Get(int id) { - try - { - return await _repository.Get(id); - } - catch (ItemNotFound) - { + T ret = await _repository.GetOrDefault(id); + if (ret == null) return NotFound(); - } + return ret; } [HttpGet("{slug}")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> Get(string slug) { - try - { - return await _repository.Get(slug); - } - catch (ItemNotFound) - { + T ret = await _repository.Get(slug); + if (ret == null) return NotFound(); - } + return ret; } [HttpGet("count")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public virtual async Task> GetCount([FromQuery] Dictionary where) { try @@ -68,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, @@ -98,7 +90,7 @@ namespace Kyoo.CommonApi } [HttpPost] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Create)] public virtual async Task> Create([FromBody] T resource) { try @@ -111,13 +103,13 @@ namespace Kyoo.CommonApi } catch (DuplicatedItemException) { - T existing = await _repository.Get(resource.Slug); + T existing = await _repository.GetOrDefault(resource.Slug); return Conflict(existing); } } [HttpPut] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Write)] public virtual async Task> Edit([FromQuery] bool resetOld, [FromBody] T resource) { try @@ -129,14 +121,14 @@ namespace Kyoo.CommonApi resource.ID = old.ID; return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [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; @@ -144,14 +136,14 @@ namespace Kyoo.CommonApi { return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpPut("{slug}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Write)] public virtual async Task> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) { try @@ -160,21 +152,21 @@ namespace Kyoo.CommonApi resource.ID = old.ID; return await _repository.Edit(resource, resetOld); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpDelete("{id:int}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(int id) { try { await _repository.Delete(id); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -183,14 +175,14 @@ namespace Kyoo.CommonApi } [HttpDelete("{slug}")] - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(string slug) { try { await _repository.Delete(slug); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -198,14 +190,14 @@ namespace Kyoo.CommonApi return Ok(); } - [Authorize(Policy = "Write")] + [PartialPermission(Kind.Delete)] public virtual async Task Delete(Dictionary where) { try { 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 87% rename from Kyoo/Models/DatabaseContext.cs rename to Kyoo.CommonAPI/DatabaseContext.cs index 577e9903..584230b9 100644 --- a/Kyoo/Models/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; @@ -7,17 +8,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 . @@ -63,11 +64,20 @@ 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 . /// public DbSet PeopleRoles { get; set; } + + /// + /// Episodes with a watch percentage. See + /// + public DbSet WatchedEpisodes { get; set; } /// /// Get a generic link between two resource types. @@ -82,32 +92,31 @@ namespace Kyoo { return Set>(); } - - - /// - /// A basic constructor that set default values (query tracker behaviors, mapping enums...) - /// - public DatabaseContext() - { - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; - } /// - /// Create a new . + /// The default constructor /// - /// Connection options to use (witch databse provider to use, connection strings...) - public DatabaseContext(DbContextOptions options) + protected DatabaseContext() { } + + /// + /// Create a new using specific options + /// + /// The options to use. + protected DatabaseContext(DbContextOptions options) : base(options) - { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; - } + { } + /// + /// Set basic configurations (like preventing query tracking) + /// + /// An option builder to fill. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + } + /// /// Set database parameters to support every types of Kyoo. /// @@ -116,10 +125,6 @@ namespace Kyoo { base.OnModelCreating(modelBuilder); - modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); - modelBuilder.HasPostgresEnum(); - modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); @@ -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,13 +268,16 @@ 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(); } /// /// 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) @@ -467,13 +490,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. @@ -486,5 +505,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/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/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.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index c1e14a6e..7498fc14 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; } @@ -118,14 +118,14 @@ 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, Sort sort = default, Pagination limit = default) { - return ApplyFilters(query, Get, DefaultSort, where, sort, limit); + return ApplyFilters(query, GetOrDefault, DefaultSort, where, sort, limit); } /// @@ -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, @@ -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); } } @@ -244,7 +238,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.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj new file mode 100644 index 00000000..52c9041b --- /dev/null +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -0,0 +1,41 @@ + + + + net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + + + + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + false + false + false + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + false + runtime + + + all + false + runtime + + + diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs similarity index 86% rename from Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs index c27925c9..834321b2 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs @@ -1,16 +1,18 @@ // using System; -using Kyoo; +using System.Collections.Generic; +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.Models.DatabaseMigrations.Internal +namespace Kyoo.Postgresql.Migrations { - [DbContext(typeof(DatabaseContext))] - [Migration("20210420221509_Initial")] + [DbContext(typeof(PostgresContext))] + [Migration("20210507203809_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -21,7 +23,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .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("ProductVersion", "5.0.5") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -224,6 +226,21 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -419,8 +436,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("StartYear") .HasColumnType("integer"); - b.Property("Status") - .HasColumnType("integer"); + b.Property("Status") + .HasColumnType("status"); b.Property("StudioID") .HasColumnType("integer"); @@ -497,8 +514,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("TrackIndex") .HasColumnType("integer"); - b.Property("Type") - .HasColumnType("integer"); + b.Property("Type") + .HasColumnType("stream_type"); b.HasKey("ID"); @@ -508,6 +525,58 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -620,6 +689,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -709,6 +797,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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"); @@ -779,6 +886,13 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { b.Navigation("Shows"); }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); #pragma warning restore 612, 618 } } diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs similarity index 86% rename from Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.cs index 56051cb1..678e90ee 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; +using Kyoo.Models; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Models.DatabaseMigrations.Internal +namespace Kyoo.Postgresql.Migrations { public partial class Initial : Migration { @@ -103,6 +105,24 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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 @@ -162,7 +182,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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), + 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), @@ -255,6 +275,30 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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 @@ -406,7 +450,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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) + Type = table.Column(type: "stream_type", nullable: false) }, constraints: table => { @@ -419,6 +463,31 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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", @@ -473,6 +542,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal table: "Link", column: "SecondID"); + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + migrationBuilder.CreateIndex( name: "IX_MetadataIds_EpisodeID", table: "MetadataIds", @@ -548,6 +622,17 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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) @@ -567,6 +652,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal migrationBuilder.DropTable( name: "Link"); + migrationBuilder.DropTable( + name: "Link"); + migrationBuilder.DropTable( name: "MetadataIds"); @@ -576,6 +664,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal migrationBuilder.DropTable( name: "Tracks"); + migrationBuilder.DropTable( + name: "WatchedEpisodes"); + migrationBuilder.DropTable( name: "Collections"); @@ -594,6 +685,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal migrationBuilder.DropTable( name: "Episodes"); + migrationBuilder.DropTable( + name: "Users"); + migrationBuilder.DropTable( name: "Seasons"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs similarity index 86% rename from Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs rename to Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 11d6b186..4c6ceac7 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -1,15 +1,17 @@ // using System; -using Kyoo; +using System.Collections.Generic; +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.Models.DatabaseMigrations.Internal +namespace Kyoo.Postgresql.Migrations { - [DbContext(typeof(DatabaseContext))] - partial class DatabaseContextModelSnapshot : ModelSnapshot + [DbContext(typeof(PostgresContext))] + partial class PostgresContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { @@ -19,7 +21,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .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("ProductVersion", "5.0.5") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -222,6 +224,21 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -417,8 +434,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("StartYear") .HasColumnType("integer"); - b.Property("Status") - .HasColumnType("integer"); + b.Property("Status") + .HasColumnType("status"); b.Property("StudioID") .HasColumnType("integer"); @@ -495,8 +512,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("TrackIndex") .HasColumnType("integer"); - b.Property("Type") - .HasColumnType("integer"); + b.Property("Type") + .HasColumnType("stream_type"); b.HasKey("ID"); @@ -506,6 +523,58 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -618,6 +687,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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") @@ -707,6 +795,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal 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"); @@ -777,6 +884,13 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { 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 new file mode 100644 index 00000000..4836601c --- /dev/null +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +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; + + /// + /// 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...) + /// + public PostgresContext() + { + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + 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...) + /// + /// 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) + { + if (!_skipConfigure) + { + if (_connection != null) + optionsBuilder.UseNpgsql(_connection); + else + optionsBuilder.UseNpgsql(); + if (_debugMode) + optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); + } + + base.OnConfiguring(optionsBuilder); + } + + /// + /// 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(); + + modelBuilder.Entity() + .Property(x => x.ExtraData) + .HasColumnType("jsonb"); + + base.OnModelCreating(modelBuilder); + } + + /// + protected override bool IsDuplicateException(Exception ex) + { + 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 new file mode 100644 index 00000000..7a818296 --- /dev/null +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -0,0 +1,82 @@ +using System; +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; + +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 ICollection Provides => new[] + { + typeof(DatabaseContext) + }; + + /// + public ICollection ConditionalProvides => ArraySegment.Empty; + + /// + public ICollection Requires => ArraySegment.Empty; + + + /// + /// 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) + { + _configuration = configuration; + _environment = env; + } + + /// + public void Configure(IServiceCollection services, ICollection availableTypes) + { + 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()); + } + + /// + public void Initialize(IServiceProvider provider) + { + DatabaseContext context = provider.GetRequiredService(); + context.Database.Migrate(); + } + } +} \ 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.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.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.WebApp b/Kyoo.WebApp index da35a725..22a02671 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3 +Subproject commit 22a02671918201d6d9d4e80a76f01b59b216a82d 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.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.sln b/Kyoo.sln index 0eb53fe3..3f814bd3 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -7,6 +7,10 @@ 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 +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 @@ -29,5 +33,13 @@ 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 + {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/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/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/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/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 5026e7c3..321d2f57 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -4,100 +4,235 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; -using Kyoo.Models; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; 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 { + /// + /// The service provider. It allow plugin's activation. + /// private readonly IServiceProvider _provider; + /// + /// 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 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(IServiceProvider provider, + IConfiguration config, + ILogger logger) { _provider = provider; _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() + /// + /// 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) { 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); + plugins = plugins.Concat(pluginsPaths.SelectMany(LoadPlugin)) + .GroupBy(x => x.Name) + .Select(x => x.First()) + .ToList(); + + ICollection available = GetProvidedTypes(plugins); + _plugins.AddRange(plugins.Where(plugin => { - path = Path.GetFullPath(path); - try - { - PluginDependencyLoader loader = new(path); - Assembly ass = loader.LoadFromAssemblyPath(path); - return ass.GetTypes() - .Where(x => typeof(IPlugin).IsAssignableFrom(x)) - .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)); - } - catch (Exception ex) - { - Console.Error.WriteLine($"\nError loading the plugin at {path}.\n{ex.GetType().Name}: {ex.Message}\n"); - return Array.Empty(); - } - }).ToList(); + Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); + if (missing == null) + return true; + + _logger.LogCritical("No {Dependency} available in Kyoo but the plugin {Plugin} requires it", + missing.Name, plugin.Name); + 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(_plugins); + 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. + /// + /// The list of plugins that will be used as a plugin pool to get provided types. + /// The list of types available. + private ICollection GetProvidedTypes(ICollection plugins) + { + 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) { - Console.WriteLine("\nNo plugin enabled.\n"); - return; + if (!conditional.Condition.Condition()) + return false; + + 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))) + .ToList(); + if (!needed.Any()) + return true; + 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; } - Console.WriteLine("\nPlugin enabled:"); - foreach (IPlugin plugin in _plugins) - Console.WriteLine($"\t{plugin.Name}"); - Console.WriteLine(); + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + 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. + /// + 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) + { + 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; + // 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); + 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/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/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 1af6c485..8452b950 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; } @@ -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(); @@ -233,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/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 43afd407..703ece0b 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; } @@ -105,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(); @@ -115,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(); /// @@ -158,7 +149,7 @@ namespace Kyoo.Controllers sort, limit); if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null) - throw new ItemNotFound(); + throw new ItemNotFoundException(); return items; } @@ -173,7 +164,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/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index b4cab3b9..d569f6fe 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(); @@ -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 526a3286..452f59eb 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 { @@ -36,15 +35,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; } @@ -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(); @@ -74,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; }); @@ -130,8 +129,8 @@ namespace Kyoo.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.Get(showID) == null) - throw new ItemNotFound(); + if (!people.Any() && await _shows.Value.GetOrDefault(showID) == null) + throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -152,8 +151,8 @@ namespace Kyoo.Controllers where, sort, limit); - if (!people.Any() && await _shows.Value.Get(showSlug) == null) - throw new ItemNotFound(); + if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null) + throw new ItemNotFoundException(); foreach (PeopleRole role in people) role.ForPeople = true; return people; @@ -173,8 +172,8 @@ namespace Kyoo.Controllers where, sort, limit); - if (!roles.Any() && await Get(id) == null) - throw new ItemNotFound(); + if (!roles.Any() && await GetOrDefault(id) == null) + throw new ItemNotFoundException(); return roles; } @@ -192,8 +191,8 @@ namespace Kyoo.Controllers where, sort, limit); - if (!roles.Any() && await Get(slug) == null) - throw new ItemNotFound(); + if (!roles.Any() && await GetOrDefault(slug) == null) + throw new ItemNotFoundException(); return roles; } } 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 e35042ad..289ca08f 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; } @@ -89,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; } @@ -99,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; } @@ -122,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(); @@ -161,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 1129cd07..eb3f36e4 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 { @@ -15,7 +14,7 @@ namespace Kyoo.Controllers public class ShowRepository : LocalRepository, IShowRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; /// @@ -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; } @@ -78,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(); @@ -103,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/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/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 55ddb427..4b27a5e4 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; } @@ -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/Controllers/Repositories/UserRepository.cs b/Kyoo/Controllers/Repositories/UserRepository.cs new file mode 100644 index 00000000..5ebd4d2b --- /dev/null +++ b/Kyoo/Controllers/Repositories/UserRepository.cs @@ -0,0 +1,64 @@ +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 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) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + _database.Entry(obj).State = EntityState.Deleted; + await _database.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 519fb43f..5f281ba3 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -1,56 +1,119 @@ 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 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; 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 IServiceProvider _provider; + /// + /// The configuration instance used to get schedule information + /// 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 readonly List<(ITask task, DateTime scheduledDate)> _tasks; + /// + /// The queue of tasks that should be run 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)>(); - - public TaskManager(IServiceProvider serviceProvider, IPluginManager pluginManager, IConfiguration configuration) + /// + /// The cancellation token used to cancel the running task when the runner should shutdown. + /// + private readonly CancellationTokenSource _taskToken = new(); + + + /// + /// 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, + IServiceProvider provider, + IConfiguration configuration, + ILogger logger) { - _serviceProvider = serviceProvider; - _pluginManager = pluginManager; - _configuration = configuration; + _provider = provider; + _configuration = configuration.GetSection("scheduledTasks"); + _logger = logger; + _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)); + else + _logger.LogInformation("Task manager initiated without any tasks"); } + + /// + /// 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); + await RunTask(task, arguments); } catch (Exception e) { - Console.Error.WriteLine($"An unhandled exception occured while running the task {task.Name}.\nInner exception: {e.Message}\n\n"); + _logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name); } } else @@ -61,67 +124,122 @@ 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); + })); + + 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); + } + + /// + /// Inject services into the marked properties of the given object. + /// + /// The object to inject + /// 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) + property.SetValue(obj, retrieve(property.PropertyType)); + } + + /// + /// 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) + .OrderByDescending(x => x.Priority); + foreach (ITask task in startupTasks) + _queuedTasks.Enqueue((task, new Dictionary())); } - 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 = null) { + arguments ??= new Dictionary(); + int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); if (index == -1) - return false; + 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)); - return true; + _tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug)); } - public TimeSpan GetTaskDelay(string taskSlug) + /// + /// Get the next date of the execution of the given task. + /// + /// The slug of the task + /// The next date. + private DateTime GetNextTaskDate(string taskSlug) { - TimeSpan delay = _configuration.GetSection("scheduledTasks").GetValue(taskSlug); + TimeSpan delay = _configuration.GetValue(taskSlug); if (delay == default) - delay = TimeSpan.FromDays(365); - return delay; + return DateTime.MaxValue; + return DateTime.Now + 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)))); - } - - public IEnumerable GetAllTasks() - { - return _tasks.Select(x => x.task); + return _tasks.Select(x => x.task).ToArray(); } } } \ No newline at end of file diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs new file mode 100644 index 00000000..3111177e --- /dev/null +++ b/Kyoo/CoreModule.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kyoo.Controllers; +using Kyoo.Models.Permissions; +using Kyoo.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo +{ + /// + /// The core module containing default implementations + /// + public class CoreModule : IPlugin + { + /// + public string Slug => "core"; + + /// + public string Name => "Core"; + + /// + public string Description => "The core module containing default implementations."; + + /// + public ICollection Provides => new[] + { + typeof(IFileManager), + typeof(ITranscoder), + typeof(IThumbnailsManager), + typeof(IProviderManager), + typeof(ITaskManager), + typeof(ILibraryManager) + }; + + /// + public ICollection ConditionalProvides => new ConditionalProvide[] + { + (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)), + (typeof(IUserRepository), typeof(DatabaseContext)) + }; + + /// + 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) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(x => x.GetService() as TaskManager); + + services.AddScoped(); + + if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes)) + { + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + services.AddRepository(); + } + + 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 2d624b9c..22fd6e21 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/** @@ -35,36 +34,25 @@ - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + + + + + + + + + + - @@ -84,18 +72,13 @@ - + wwwroot/%(StaticFiles.RecursiveDir)%(StaticFiles.Filename)%(StaticFiles.Extension) PreserveNewest true - - login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) - PreserveNewest - true - @@ -103,9 +86,8 @@ - + - @@ -124,4 +106,25 @@ false + + + + + ../Kyoo.WebLogin/ + + + + + + + + login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension) + PreserveNewest + true + + + + + + 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/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/Models/User.cs b/Kyoo/Models/User.cs deleted file mode 100644 index 2a65d409..00000000 --- a/Kyoo/Models/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/Program.cs b/Kyoo/Program.cs index 1fa2b7c3..9fecc61b 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -1,14 +1,12 @@ 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; - namespace Kyoo { /// @@ -39,15 +37,23 @@ 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 Console.WriteLine($"Running as {Environment.UserName}."); - IWebHostBuilder host = CreateWebHostBuilder(args); + IWebHostBuilder builder = CreateWebHostBuilder(args); if (debug != null) - host = host.UseEnvironment(debug == true ? "Development" : "Production"); - await host.Build().RunAsync(); + builder = builder.UseEnvironment(debug == true ? "Development" : "Production"); + try + { + await builder.Build().RunAsync(); + } + catch (Exception ex) + { + await Console.Error.WriteLineAsync($"Unhandled exception: {ex}"); + } } /// @@ -62,16 +68,14 @@ namespace Kyoo .AddEnvironmentVariables() .AddCommandLine(args); } - + /// - /// Createa a web host + /// Create a a web host /// /// Command line parameters that can be handled by kestrel /// A new web host instance private static IWebHostBuilder CreateWebHostBuilder(string[] args) { - WebHost.CreateDefaultBuilder(args); - return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build()) @@ -79,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 8e1485e3..e30f495e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,20 +1,14 @@ using System; using System.IO; -using System.Reflection; -using IdentityServer4.Extensions; -using IdentityServer4.Services; -using Kyoo.Api; +using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Postgresql; +using Kyoo.Tasks; 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; @@ -28,20 +22,48 @@ 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) + /// + /// 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, IWebHostEnvironment host) { _configuration = configuration; - _loggerFactory = loggerFactory; + _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger()); + + // 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) + }); } + /// + /// Configure the WebApp services context. + /// + /// The service collection to fill. public void ConfigureServices(IServiceCollection services) { - string publicUrl = _configuration.GetValue("public_url"); + string publicUrl = _configuration.GetValue("publicUrl"); + services.AddMvc().AddControllersAsServices(); + services.AddSpaStaticFiles(configuration => { configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot"); @@ -58,139 +80,23 @@ namespace Kyoo x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); }); 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()); - }); - - 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(), - sql => sql.MigrationsAssembly(assemblyName)); - }) - .AddOperationalStore(options => - { - options.ConfigureDbContext = builder => - builder.UseNpgsql(_configuration.GetDatabaseConnection(), - sql => sql.MigrationsAssembly(assemblyName)); - options.EnableTokenCleanup = true; - }) - .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) - .AddInMemoryApiScopes(IdentityContext.GetScopes()) - .AddInMemoryApiResources(IdentityContext.GetApis()) - .AddProfileService() - .AddSigninKeys(_configuration); + services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>)); - 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) } - }); - - - // 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.AddSingleton(_plugins); + services.AddTask(); + _plugins.ConfigureServices(services); } - + + /// + /// 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(); - } else { app.UseExceptionHandler("/Error"); @@ -222,28 +128,19 @@ 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(); + + _plugins.ConfigureAspnet(app); app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllers(); }); - + + app.UseSpa(spa => { spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); - + if (env.IsDevelopment()) spa.UseAngularCliServer("start"); }); 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..e6a6cebf 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -7,10 +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 { @@ -21,19 +23,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 +45,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()) @@ -74,7 +73,7 @@ namespace Kyoo.Controllers 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}"); @@ -148,10 +147,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); @@ -178,7 +177,7 @@ namespace Kyoo.Controllers 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}."); } @@ -195,10 +194,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); @@ -254,10 +253,10 @@ namespace Kyoo.Controllers { 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); + collection = await MetadataProvider.GetCollectionFromName(collectionName, library); try { @@ -266,7 +265,7 @@ namespace Kyoo.Controllers } catch (DuplicatedItemException) { - return await libraryManager.Get(collection.Slug); + return await libraryManager.GetOrDefault(collection.Slug); } } @@ -276,15 +275,15 @@ namespace Kyoo.Controllers 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); 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 { @@ -292,7 +291,7 @@ namespace Kyoo.Controllers } 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); @@ -301,7 +300,7 @@ namespace Kyoo.Controllers show.Slug += $"-{show.StartYear}"; await libraryManager.Create(show); } - await _thumbnailsManager.Validate(show); + await ThumbnailsManager.Validate(show); return show; } @@ -318,11 +317,18 @@ namespace Kyoo.Controllers season.Show = show; return season; } - catch (ItemNotFound) + catch (ItemNotFoundException) { - Season season = await _metadataProvider.GetSeason(show, seasonNumber, library); - await libraryManager.CreateIfNotExists(season); - await _thumbnailsManager.Validate(season); + Season season = await MetadataProvider.GetSeason(show, seasonNumber, library); + try + { + await libraryManager.Create(season); + await ThumbnailsManager.Validate(season); + } + catch (DuplicatedItemException) + { + season = await libraryManager.Get(show.Slug, seasonNumber); + } season.Show = show; return season; } @@ -336,7 +342,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 +352,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 +373,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/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 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/AccountApi.cs b/Kyoo/Views/AccountApi.cs deleted file mode 100644 index dc02fcda..00000000 --- a/Kyoo/Views/AccountApi.cs +++ /dev/null @@ -1,213 +0,0 @@ -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.AspNetCore.StaticFiles; -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; } - } - - [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 - { - 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 User {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 List - { - 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/Views/CollectionApi.cs b/Kyoo/Views/CollectionApi.cs index 0d5b0be5..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, @@ -40,7 +41,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); } @@ -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, @@ -92,7 +93,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 +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 8dd28956..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,56 +34,68 @@ 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) { - 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")] + [PartialPermission(Kind.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")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetSeason(string showSlug, int seasonNumber, int episodeNumber) { try { return await _libraryManager.Get(showSlug, seasonNumber); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [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 { return await _libraryManager.Get(showID, seasonNumber); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -90,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, @@ -104,7 +117,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); } @@ -116,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, @@ -146,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, @@ -175,7 +188,6 @@ namespace Kyoo.Api } [HttpGet("{id:int}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(int id) { try @@ -183,14 +195,13 @@ namespace Kyoo.Api Episode episode = await _libraryManager.Get(id); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/thumb")] - [Authorize(Policy="Read")] public async Task GetThumb(string slug) { try @@ -198,7 +209,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/GenreApi.cs b/Kyoo/Views/GenreApi.cs index ab40f20b..83ebb38d 100644 --- a/Kyoo/Views/GenreApi.cs +++ b/Kyoo/Views/GenreApi.cs @@ -5,8 +5,7 @@ using System.Threading.Tasks; 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; @@ -15,6 +14,7 @@ namespace Kyoo.Api [Route("api/genre")] [Route("api/genres")] [ApiController] + [PartialPermission(nameof(GenreApi))] public class GenreApi : CrudApi { private readonly ILibraryManager _libraryManager; @@ -27,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, @@ -41,7 +41,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); } @@ -53,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, @@ -67,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 93081db6..239dcd8e 100644 --- a/Kyoo/Views/LibraryApi.cs +++ b/Kyoo/Views/LibraryApi.cs @@ -6,8 +6,7 @@ using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Kyoo.CommonApi; -using Kyoo.Models.Exceptions; -using Microsoft.AspNetCore.Authorization; +using Kyoo.Models.Permissions; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -15,6 +14,7 @@ namespace Kyoo.Api [Route("api/library")] [Route("api/libraries")] [ApiController] + [PartialPermission(nameof(LibraryAPI))] public class LibraryAPI : CrudApi { private readonly ILibraryManager _libraryManager; @@ -27,18 +27,18 @@ namespace Kyoo.Api _taskManager = taskManager; } - [Authorize(Policy = "Admin")] + [PartialPermission(Kind.Create)] public override async Task> Create(Library resource) { 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; } [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 +52,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); } @@ -64,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, @@ -78,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); } @@ -90,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, @@ -104,7 +104,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); } @@ -116,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, @@ -130,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); } @@ -142,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, @@ -156,7 +156,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); } @@ -168,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, @@ -182,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 9f97a275..6cd81b05 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; @@ -25,11 +25,11 @@ namespace Kyoo.Api public LibraryItemApi(ILibraryItemRepository libraryItems, IConfiguration configuration) { _libraryItems = libraryItems; - _baseURL = configuration.GetValue("public_url").TrimEnd('/'); + _baseURL = configuration.GetValue("publicUrl").TrimEnd('/'); } [HttpGet] - [Authorize(Policy = "Read")] + [Permission(nameof(LibraryItemApi), Kind.Read)] public async Task>> GetAll([FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, @@ -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..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, @@ -48,7 +49,7 @@ namespace Kyoo.Api return Page(resources, limit); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -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, @@ -76,7 +77,7 @@ namespace Kyoo.Api return Page(resources, limit); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -87,18 +88,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..133c15fd 100644 --- a/Kyoo/Views/ProviderApi.cs +++ b/Kyoo/Views/ProviderApi.cs @@ -2,7 +2,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; @@ -11,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) @@ -29,18 +30,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/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 9803f956..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, @@ -47,7 +48,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); } @@ -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,40 +114,51 @@ 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) { - 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 de623916..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, @@ -49,7 +50,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); } @@ -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, @@ -75,7 +76,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); } @@ -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, @@ -101,7 +102,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); } @@ -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, @@ -127,7 +128,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); } @@ -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, @@ -152,7 +153,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); } @@ -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, @@ -177,7 +178,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); } @@ -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, @@ -203,7 +204,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); } @@ -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, @@ -229,7 +230,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); } @@ -240,28 +241,28 @@ namespace Kyoo.Api } [HttpGet("{showID:int}/studio")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetStudio(int showID) { try { return await _libraryManager.Get(x => x.Shows.Any(y => y.ID == showID)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/studio")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetStudio(string slug) { try { return await _libraryManager.Get(x => x.Shows.Any(y => y.Slug == slug)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -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, @@ -283,7 +284,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); } @@ -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, @@ -309,7 +310,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); } @@ -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, @@ -335,7 +336,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); } @@ -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, @@ -361,7 +362,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); } @@ -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 @@ -384,7 +385,7 @@ namespace Kyoo.Api .ToDictionary(Path.GetFileNameWithoutExtension, x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -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 @@ -401,14 +402,13 @@ namespace Kyoo.Api string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); return _files.FileResult(path); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/poster")] - [Authorize(Policy = "Read")] public async Task GetPoster(string slug) { try @@ -416,14 +416,13 @@ namespace Kyoo.Api Show show = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetShowPoster(show)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/logo")] - [Authorize(Policy="Read")] public async Task GetLogo(string slug) { try @@ -431,14 +430,13 @@ namespace Kyoo.Api Show show = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetShowLogo(show)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/backdrop")] - [Authorize(Policy="Read")] public async Task GetBackdrop(string slug) { try @@ -446,7 +444,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/StudioApi.cs b/Kyoo/Views/StudioApi.cs index 45f46829..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, @@ -40,7 +41,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); } @@ -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/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 4ae053de..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; @@ -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) { 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..b75033d7 --- /dev/null +++ b/Kyoo/Views/TaskApi.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Kyoo.Controllers; +using Kyoo.Models.Exceptions; +using Kyoo.Models.Permissions; + +namespace Kyoo.Api +{ + [Route("api/task")] + [Route("api/tasks")] + [ApiController] + public class TaskApi : ControllerBase + { + private readonly ITaskManager _taskManager; + + public TaskApi(ITaskManager taskManager) + { + _taskManager = taskManager; + } + + + [HttpGet] + [Permission(nameof(TaskApi), Kind.Read)] + public ActionResult> GetTasks() + { + return Ok(_taskManager.GetAllTasks()); + } + + [HttpGet("{taskSlug}")] + [HttpPut("{taskSlug}")] + [Permission(nameof(TaskApi), Kind.Create)] + public IActionResult RunTask(string taskSlug, [FromQuery] Dictionary args) + { + try + { + _taskManager.StartTask(taskSlug, args); + return Ok(); + } + catch (ItemNotFoundException) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} diff --git a/Kyoo/Views/TrackApi.cs b/Kyoo/Views/TrackApi.cs index 79bced62..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,21 +25,21 @@ namespace Kyoo.Api } [HttpGet("{id:int}/episode")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetEpisode(int id) { try { return await _libraryManager.Get(x => x.Tracks.Any(y => y.ID == id)); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("{slug}/episode")] - [Authorize(Policy = "Read")] + [PartialPermission(Kind.Read)] public async Task> GetEpisode(string slug) { try @@ -47,7 +48,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..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 @@ -52,14 +52,14 @@ namespace Kyoo.Api Episode episode = await _libraryManager.Get(slug); return _files.FileResult(episode.Path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("transmux/{slug}/master.m3u8")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public async Task Transmux(string slug) { try @@ -71,14 +71,14 @@ namespace Kyoo.Api return StatusCode(500); return _files.FileResult(path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } } [HttpGet("transcode/{slug}/master.m3u8")] - [Authorize(Policy="Play")] + [Permission("video", Kind.Read)] public async Task Transcode(string slug) { try @@ -90,7 +90,7 @@ namespace Kyoo.Api return StatusCode(500); return _files.FileResult(path, true); } - catch (ItemNotFound) + catch (ItemNotFoundException) { return NotFound(); } @@ -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 cd9327ae..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 @@ -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 bdd2f362..fed2bd58 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -1,42 +1,56 @@ { "server.urls": "http://*:5000", - "public_url": "http://localhost:5000/", + "publicUrl": "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": { "logLevel": { "default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "None", + "Kyoo": "Trace" } }, + "authentication": { + "certificate": { + "file": "certificate.pfx", + "oldFile": "oldCertificate.pfx", + "password": "passphrase" + }, + "permissions": { + "default": ["overall.read", "overall.write", "overall.create", "overall.delete"], + "newUser": ["overall.read", "overall.write", "overall.create", "overall.delete"] + }, + "profilePicturePath": "users/", + "clients": [] + }, + + "parallelTasks": "1", "scheduledTasks": { "scan": "24:00:00" }, - - "certificatePassword": "passphrase", "transmuxTempPath": "cached/kyoo/transmux", "transcodeTempPath": "cached/kyoo/transcode", "peoplePath": "people", "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\\.)?.*$" }