Reworking login requests

This commit is contained in:
Zoe Roux 2021-05-07 01:45:26 +02:00
parent 0f2bea9bc4
commit d9cca97961
17 changed files with 523 additions and 221 deletions

View File

@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using IdentityModel;
namespace Kyoo.Authentication
{
public static class PasswordUtils
{
/// <summary>
/// Generate an OneTimeAccessCode.
/// </summary>
/// <returns>A new otac.</returns>
public static string GenerateOTAC()
{
return CryptoRandom.CreateUniqueId();
}
/// <summary>
/// Hash a password to store it has a verification only.
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
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);
}
/// <summary>
/// Check if a password is the same as a valid hashed password.
/// </summary>
/// <param name="password">The password to check</param>
/// <param name="validPassword">
/// The valid hashed password. This password must be hashed via <see cref="HashPassword"/>.
/// </param>
/// <returns>True if the password is valid, false otherwise.</returns>
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));
}
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Security.Claims;
using IdentityModel;
using Kyoo.Models;
namespace Kyoo.Authentication
{
/// <summary>
/// Extension methods.
/// </summary>
public static class Extensions
{
public static ClaimsPrincipal ToPrincipal(this User user)
{
List<Claim> claims = new()
{
new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
new Claim(JwtClaimTypes.Name, user.Username),
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
};
ClaimsIdentity id = new (claims);
return new ClaimsPrincipal(id);
}
}
}

View File

@ -19,6 +19,7 @@
<PackageReference Include="IdentityServer4" Version="4.1.2" /> <PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.Storage" Version="4.1.2" /> <PackageReference Include="IdentityServer4.Storage" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" /> <PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,28 @@
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model only used on login requests.
/// </summary>
public class LoginRequest
{
/// <summary>
/// The user's username.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The user's password.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Should the user stay logged in? If true a cookie will be put.
/// </summary>
public bool StayLoggedIn { get; set; }
/// <summary>
/// The return url of the login flow.
/// </summary>
public string ReturnURL { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model to represent an otac request
/// </summary>
public class OtacRequest
{
/// <summary>
/// The One Time Access Code
/// </summary>
public string Otac { get; set; }
/// <summary>
/// Should the user stay logged
/// </summary>
public bool StayLoggedIn { get; set; }
}
}

View File

@ -0,0 +1,28 @@
namespace Kyoo.Authentication.Models
{
/// <summary>
/// The main authentication options.
/// </summary>
public class AuthenticationOptions
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "authentication";
/// <summary>
/// The options for certificates
/// </summary>
public CertificateOption Certificate { get; set; }
/// <summary>
/// Options for permissions
/// </summary>
public PermissionOption Permissions { get; set; }
/// <summary>
/// Root path of user's profile pictures.
/// </summary>
public string ProfilePicturePath { get; set; }
}
}

View File

@ -15,11 +15,11 @@ namespace Kyoo.Authentication.Models
/// <summary> /// <summary>
/// The default permissions that will be given to a non-connected user. /// The default permissions that will be given to a non-connected user.
/// </summary> /// </summary>
public ICollection<string> Default { get; set; } public string[] Default { get; set; }
/// <summary> /// <summary>
/// Permissions applied to a new user. /// Permissions applied to a new user.
/// </summary> /// </summary>
public ICollection<string> NewUser { get; set; } public string[] NewUser { get; set; }
} }
} }

View File

@ -1,183 +1,218 @@
// using System; using System;
// using System.Collections.Generic; using System.Collections.Generic;
// using System.IO; using System.Globalization;
// using System.Linq; using System.IO;
// using System.Security.Claims; using System.Security.Claims;
// using System.Threading.Tasks; using System.Threading.Tasks;
// using IdentityServer4.Models; using IdentityModel;
// using IdentityServer4.Services; using IdentityServer4.Models;
// using Kyoo.Authentication.Models.DTO; using IdentityServer4.Services;
// using Kyoo.Models; using Kyoo.Authentication.Models.DTO;
// using Microsoft.AspNetCore.Authorization; using Kyoo.Controllers;
// using Microsoft.AspNetCore.Http; using Kyoo.Models;
// using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization;
// using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authentication;
// using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Http;
// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; using Microsoft.AspNetCore.Mvc;
// using Microsoft.Extensions.Options;
// namespace Kyoo.Authentication.Views using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
// {
// public class LoginRequest namespace Kyoo.Authentication.Views
// { {
// public string Username { get; set; } public class AccountData
// public string Password { get; set; } {
// public bool StayLoggedIn { get; set; } [FromForm(Name = "email")]
// } public string Email { get; set; }
// [FromForm(Name = "username")]
// public class OtacRequest public string Username { get; set; }
// { [FromForm(Name = "picture")]
// public string Otac { get; set; } public IFormFile Picture { get; set; }
// public bool StayLoggedIn { get; set; } }
// }
//
// public class AccountData /// <summary>
// { /// The class responsible for login, logout, permissions and claims of a user.
// [FromForm(Name = "email")] /// </summary>
// public string Email { get; set; } [Route("api/account")]
// [FromForm(Name = "username")] [Route("api/accounts")]
// public string Username { get; set; } [ApiController]
// [FromForm(Name = "picture")] public class AccountApi : Controller, IProfileService
// public IFormFile Picture { get; set; } {
// } /// <summary>
// /// The repository to handle users.
// /// </summary>
// /// <summary> private readonly IUserRepository _users;
// /// The class responsible for login, logout, permissions and claims of a user. /// <summary>
// /// </summary> /// The identity server interaction service to login users.
// [Route("api/account")] /// </summary>
// [Route("api/accounts")] private readonly IIdentityServerInteractionService _interaction;
// [ApiController] /// <summary>
// public class AccountApi : Controller, IProfileService /// Options about authentication. Those options are monitored and reloads are supported.
// { /// </summary>
// private readonly UserManager<User> _userManager; private readonly IOptionsMonitor<AuthenticationOptions> _options;
// private readonly SignInManager<User> _signInManager;
// private readonly IConfiguration _configuration;
// private readonly string _picturePath; /// <summary>
// /// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
// // TODO find how SignInManager & UserManager are implement and check if they can be used or not. /// </summary>
// public AccountApi(UserManager<User> userManager, /// <param name="users">The user repository to create and manage users</param>
// SignInManager<User> signInManager, /// <param name="interaction">The identity server interaction service to login users.</param>
// IConfiguration configuration) /// <param name="options">Authentication options (this may be hot reloaded)</param>
// { public AccountApi(IUserRepository users,
// _userManager = userManager; IIdentityServerInteractionService interaction,
// _signInManager = signInManager; IOptionsMonitor<AuthenticationOptions> options)
// _picturePath = configuration.GetValue<string>("profilePicturePath"); {
// _configuration = configuration; _users = users;
// if (!Path.IsPathRooted(_picturePath)) _interaction = interaction;
// _picturePath = Path.GetFullPath(_picturePath); _options = options;
// } }
//
// [HttpPost("register")]
// public async Task<IActionResult> Register([FromBody] RegisterRequest request) /// <summary>
// { /// Register a new user and return a OTAC to connect to it.
// User user = request.ToUser(); /// </summary>
// IdentityResult result = await _userManager.CreateAsync(user, user.Password); /// <param name="request">The DTO register request</param>
// if (!result.Succeeded) /// <returns>A OTAC to connect to this new account</returns>
// return BadRequest(result.Errors); [HttpPost("register")]
// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1)); public async Task<ActionResult<string>> Register([FromBody] RegisterRequest request)
// await _userManager.UpdateAsync(account); {
// await _userManager.AddClaimAsync(account, new Claim( User user = request.ToUser();
// "permissions", user.Permissions = _options.CurrentValue.Permissions.NewUser;
// _configuration.GetValue<string>("newUserPermissions"))); user.Password = PasswordUtils.HashPassword(user.Password);
// return Ok(new {otac}); user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
// } user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
// await _users.Create(user);
// [HttpPost("login")] return user.ExtraData["otac"];
// public async Task<IActionResult> Login([FromBody] LoginRequest login) }
// {
// if (!ModelState.IsValid) /// <summary>
// return BadRequest(login); /// Return an authentication properties based on a stay login property
// SignInResult result = await _signInManager /// </summary>
// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false); /// <param name="stayLogged">Should the user stay logged</param>
// if (result.Succeeded) /// <returns>Authentication properties based on a stay login</returns>
// return Ok(); private static AuthenticationProperties StayLogged(bool stayLogged)
// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}}); {
// } if (!stayLogged)
// return null;
// [HttpPost("otac-login")] return new AuthenticationProperties
// public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac) {
// { IsPersistent = true,
// if (!ModelState.IsValid) ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
// 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."}}); /// <summary>
// if (user.OTACExpires <= DateTime.UtcNow) /// Login the user.
// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}}); /// </summary>
// await _signInManager.SignInAsync(user, otac.StayLoggedIn); /// <param name="login">The DTO login request</param>
// return Ok(); [HttpPost("login")]
// } public async Task<IActionResult> Login([FromBody] LoginRequest login)
// {
// [HttpGet("logout")] AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL);
// [Authorize] User user = await _users.Get(x => x.Username == login.Username);
// public async Task<IActionResult> Logout()
// { if (context == null || user == null)
// await _signInManager.SignOutAsync(); return Unauthorized();
// return Ok(); if (!PasswordUtils.CheckPassword(login.Password, user.Password))
// } return Unauthorized();
//
// public async Task GetProfileDataAsync(ProfileDataRequestContext context) await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(login.StayLoggedIn));
// { return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
// User user = await _userManager.GetUserAsync(context.Subject); }
// if (user != null)
// { /// <summary>
// List<Claim> claims = new() /// Use a OTAC to login a user.
// { /// </summary>
// new Claim("email", user.Email), /// <param name="otac">The OTAC request</param>
// new Claim("username", user.UserName), [HttpPost("otac-login")]
// new Claim("picture", $"api/account/picture/{user.UserName}") public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
// }; {
// User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac);
// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions"); if (user == null)
// if (perms != null) return Unauthorized();
// claims.Add(perms); if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow)
// return BadRequest(new
// context.IssuedClaims.AddRange(claims); {
// } code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
// } });
// await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(otac.StayLoggedIn));
// public async Task IsActiveAsync(IsActiveContext context) return Ok();
// { }
// User user = await _userManager.GetUserAsync(context.Subject);
// context.IsActive = user != null; /// <summary>
// } /// Sign out an user
// /// </summary>
// [HttpGet("picture/{username}")] [HttpGet("logout")]
// public async Task<IActionResult> GetPicture(string username) [Authorize]
// { public async Task<IActionResult> Logout()
// User user = await _userManager.FindByNameAsync(username); {
// if (user == null) await HttpContext.SignOutAsync();
// return BadRequest(); return Ok();
// string path = Path.Combine(_picturePath, user.Id); }
// if (!System.IO.File.Exists(path))
// return NotFound(); // TODO check with the extension method
// return new PhysicalFileResult(path, "image/png"); public async Task GetProfileDataAsync(ProfileDataRequestContext context)
// } {
// User user = await _userManager.GetUserAsync(context.Subject);
// [HttpPost("update")] if (user != null)
// [Authorize] {
// public async Task<IActionResult> Update([FromForm] AccountData data) List<Claim> claims = new()
// { {
// User user = await _userManager.GetUserAsync(HttpContext.User); new Claim(JwtClaimTypes.Email, user.Email),
// new Claim(JwtClaimTypes.Name, user.Username),
// if (!string.IsNullOrEmpty(data.Email)) new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
// user.Email = data.Email; };
// if (!string.IsNullOrEmpty(data.Username))
// user.UserName = data.Username; Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
// if (data.Picture?.Length > 0) if (perms != null)
// { claims.Add(perms);
// string path = Path.Combine(_picturePath, user.Id);
// await using FileStream file = System.IO.File.Create(path); context.IssuedClaims.AddRange(claims);
// await data.Picture.CopyToAsync(file); }
// } }
// await _userManager.UpdateAsync(user);
// return Ok(); public async Task IsActiveAsync(IsActiveContext context)
// } {
// User user = await _userManager.GetUserAsync(context.Subject);
// [HttpGet("default-permissions")] context.IsActive = user != null;
// public ActionResult<IEnumerable<string>> GetDefaultPermissions() }
// {
// return _configuration.GetValue<string>("defaultPermissions").Split(","); [HttpGet("picture/{username}")]
// } public async Task<IActionResult> 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<IActionResult> 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<IEnumerable<string>> GetDefaultPermissions()
{
return _configuration.GetValue<string>("defaultPermissions").Split(",");
}
}
}

View File

@ -11,7 +11,7 @@ using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public readonly struct Pagination public readonly struct Pagination
{ {
@ -44,7 +44,7 @@ namespace Kyoo.Controllers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <typeparam name="T">For witch type this sort applies</typeparam> /// <typeparam name="T">For witch type this sort applies</typeparam>
public readonly struct Sort<T> public readonly struct Sort<T>
@ -54,7 +54,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
public Expression<Func<T, object>> Key { get; } public Expression<Func<T, object>> Key { get; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public bool Descendant { get; } public bool Descendant { get; }
@ -175,7 +175,7 @@ namespace Kyoo.Controllers
/// Get every resources that match all filters /// Get every resources that match all filters
/// </summary> /// </summary>
/// <param name="where">A filter predicate</param> /// <param name="where">A filter predicate</param>
/// <param name="sort">Sort informations about the query (sort by, sort order)</param> /// <param name="sort">Sort information about the query (sort by, sort order)</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param> /// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <returns>A list of resources that match every filters</returns> /// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
@ -205,24 +205,24 @@ namespace Kyoo.Controllers
/// Create a new resource. /// Create a new resource.
/// </summary> /// </summary>
/// <param name="obj">The item to register</param> /// <param name="obj">The item to register</param>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns> /// <returns>The resource registers and completed by database's information (related items & so on)</returns>
Task<T> Create([NotNull] T obj); Task<T> Create([NotNull] T obj);
/// <summary> /// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead. /// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary> /// </summary>
/// <param name="obj">The object to create</param> /// <param name="obj">The object to create</param>
/// <param name="silentFail">Allow issues to occurs in this method. Every issue is catched and ignored.</param> /// <param name="silentFail">Allow issues to occurs in this method. Every issue is caught and ignored.</param>
/// <returns>The newly created item or the existing value if it existed.</returns> /// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false); Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false);
/// <summary> /// <summary>
/// Edit a resource /// Edit a resource
/// </summary> /// </summary>
/// <param name="edited">The resourcce to edit, it's ID can't change.</param> /// <param name="edited">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's information (related items & so on)</returns>
Task<T> Edit([NotNull] T edited, bool resetOld); Task<T> Edit([NotNull] T edited, bool resetOld);
/// <summary> /// <summary>
@ -259,25 +259,25 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">One or multiple resources's id</param> /// <param name="ids">One or multiple resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">An enumearble of resources's id</param> /// <param name="ids">An enumerable of resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids); Task DeleteRange(IEnumerable<int> ids);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">One or multiple resources's slug</param> /// <param name="slugs">One or multiple resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">An enumerable of resources's slug</param> /// <param name="slugs">An enumerable of resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs); Task DeleteRange(IEnumerable<string> slugs);
/// <summary> /// <summary>
@ -294,7 +294,7 @@ namespace Kyoo.Controllers
public interface IShowRepository : IRepository<Show> public interface IShowRepository : IRepository<Show>
{ {
/// <summary> /// <summary>
/// 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. /// If both a library and a collection are given, the collection is added to the library too.
/// </summary> /// </summary>
/// <param name="showID">The ID of the show</param> /// <param name="showID">The ID of the show</param>
@ -421,7 +421,7 @@ namespace Kyoo.Controllers
/// <param name="slug">The slug of the track</param> /// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The tracl found</returns> /// <returns>The track found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown); Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary> /// <summary>
@ -429,7 +429,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the track</param> /// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns> /// <returns>The track found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown); Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
} }
@ -439,16 +439,16 @@ namespace Kyoo.Controllers
public interface ILibraryRepository : IRepository<Library> { } public interface ILibraryRepository : IRepository<Library> { }
/// <summary> /// <summary>
/// A repository to handle library items (A wrapper arround shows and collections). /// A repository to handle library items (A wrapper around shows and collections).
/// </summary> /// </summary>
public interface ILibraryItemRepository : IRepository<LibraryItem> public interface ILibraryItemRepository : IRepository<LibraryItem>
{ {
/// <summary> /// <summary>
/// Get items (A wrapper arround shows or collections) from a library. /// Get items (A wrapper around shows or collections) from a library.
/// </summary> /// </summary>
/// <param name="id">The ID of the library</param> /// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id, public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
@ -456,7 +456,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary> /// <summary>
/// Get items (A wrapper arround shows or collections) from a library. /// Get items (A wrapper around shows or collections) from a library.
/// </summary> /// </summary>
/// <param name="id">The ID of the library</param> /// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
@ -470,11 +470,11 @@ namespace Kyoo.Controllers
) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit); ) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
/// <summary> /// <summary>
/// Get items (A wrapper arround shows or collections) from a library. /// Get items (A wrapper around shows or collections) from a library.
/// </summary> /// </summary>
/// <param name="slug">The slug of the library</param> /// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug, public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
@ -482,7 +482,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary> /// <summary>
/// Get items (A wrapper arround shows or collections) from a library. /// Get items (A wrapper around shows or collections) from a library.
/// </summary> /// </summary>
/// <param name="slug">The slug of the library</param> /// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
@ -521,7 +521,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showID">The ID of the show</param> /// <param name="showID">The ID of the show</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
@ -547,7 +547,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
@ -573,7 +573,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the person</param> /// <param name="id">The id of the person</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id, Task<ICollection<PeopleRole>> GetFromPeople(int id,
@ -599,7 +599,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the person</param> /// <param name="slug">The slug of the person</param>
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug, Task<ICollection<PeopleRole>> GetFromPeople(string slug,
@ -631,7 +631,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="where">A predicate to add arbitrary filter</param> /// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order & sort by)</param> /// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null, Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default, Sort<MetadataID> sort = default,
@ -642,11 +642,16 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="where">A predicate to add arbitrary filter</param> /// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">A sort by expression</param> /// <param name="sort">A sort by expression</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where, Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID, object>> sort, Expression<Func<MetadataID, object>> sort,
Pagination limit = default Pagination limit = default
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit); ) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
} }
/// <summary>
/// A repository to handle users.
/// </summary>
public interface IUserRepository : IRepository<User> {}
} }

View File

@ -28,6 +28,16 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public string Password { get; set; } public string Password { get; set; }
/// <summary>
/// The list of permissions of the user. The format of this is implementation dependent.
/// </summary>
public string[] Permissions { get; set; }
/// <summary>
/// Arbitrary extra data that can be used by specific authentication implementations.
/// </summary>
public Dictionary<string, string> ExtraData { get; set; }
/// <summary> /// <summary>
/// The list of shows the user has finished. /// The list of shows the user has finished.
/// </summary> /// </summary>
@ -36,6 +46,18 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// The list of episodes the user is watching (stopped in progress or the next episode of the show) /// The list of episodes the user is watching (stopped in progress or the next episode of the show)
/// </summary> /// </summary>
public ICollection<(Episode episode, int watchedPercentage)> CurrentlyWatching { get; set; } public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// Links between Users and Shows.
/// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; }
/// <summary>
/// Links between Users and WatchedEpisodes.
/// </summary>
public ICollection<Link<User, WatchedEpisode>> EpisodeLinks { get; set; }
#endif
} }
} }

View File

@ -0,0 +1,30 @@
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// Metadata of episode currently watching by an user
/// </summary>
public class WatchedEpisode : IResource
{
/// <inheritdoc />
[SerializeIgnore] public int ID
{
get => Episode.ID;
set => Episode.ID = value;
}
/// <inheritdoc />
[SerializeIgnore] public string Slug => Episode.Slug;
/// <summary>
/// The episode currently watched
/// </summary>
public Episode Episode { get; set; }
/// <summary>
/// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
/// </summary>
public int WatchedPercentage { get; set; }
}
}

View File

@ -215,7 +215,7 @@ namespace Kyoo
/// <summary> /// <summary>
/// An advanced <see cref="Complete{T}"/> function. /// An advanced <see cref="Complete{T}"/> function.
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>. /// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
/// 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 <see cref="IOnMerge"/>. /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary> /// </summary>
/// <param name="first">The object to complete</param> /// <param name="first">The object to complete</param>

View File

@ -67,7 +67,7 @@ namespace Kyoo
/// <summary> /// <summary>
/// The list of registered users. /// The list of registered users.
/// </summary> /// </summary>
// public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
/// <summary> /// <summary>
/// All people's role. See <see cref="PeopleRole"/>. /// All people's role. See <see cref="PeopleRole"/>.
@ -254,7 +254,7 @@ namespace Kyoo
/// Return a new or an in cache temporary object wih the same ID as the one given /// Return a new or an in cache temporary object wih the same ID as the one given
/// </summary> /// </summary>
/// <param name="model">If a resource with the same ID is found in the database, it will be used. /// <param name="model">If a resource with the same ID is found in the database, it will be used.
/// <see cref="model"/> will be used overwise</param> /// <see cref="model"/> will be used otherwise</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <returns>A resource that is now tracked by this context.</returns> /// <returns>A resource that is now tracked by this context.</returns>
public T GetTemporaryObject<T>(T model) public T GetTemporaryObject<T>(T model)

View File

@ -118,7 +118,7 @@ namespace Kyoo.Controllers
/// <param name="query">The base query to filter.</param> /// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param> /// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param> /// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>The filtered query</returns> /// <returns>The filtered query</returns>
protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query, protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
Expression<Func<T, bool>> where = null, Expression<Func<T, bool>> where = null,
@ -137,7 +137,7 @@ namespace Kyoo.Controllers
/// <param name="query">The base query to filter.</param> /// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param> /// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param> /// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>The filtered query</returns> /// <returns>The filtered query</returns>
protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query, protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query,
Func<int, Task<TValue>> get, Func<int, Task<TValue>> get,
@ -244,7 +244,7 @@ namespace Kyoo.Controllers
} }
/// <summary> /// <summary>
/// An overridable method to edit relatiosn of a resource. /// An overridable method to edit relation of a resource.
/// </summary> /// </summary>
/// <param name="resource">The non edited resource</param> /// <param name="resource">The non edited resource</param>
/// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param> /// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param>

View File

@ -33,7 +33,7 @@ namespace Kyoo.Controllers
} catch (Exception ex) } catch (Exception ex)
{ {
await Console.Error.WriteLineAsync( 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; return ret;

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
{
/// <summary>
/// A repository for users.
/// </summary>
public class UserRepository : LocalRepository<User>, IUserRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<User, object>> DefaultSort => x => x.Username;
/// <summary>
/// Create a new <see cref="UserRepository"/>
/// </summary>
/// <param name="database">The database handle to use</param>
public UserRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
public override async Task<ICollection<User>> Search(string query)
{
return await _database.Users
.Where(_database.Like<User>(x => x.Username, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
public override async Task Delete(User obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
}
}
}