diff --git a/Kyoo.Authentication/Controllers/PasswordUtils.cs b/Kyoo.Authentication/Controllers/PasswordUtils.cs
new file mode 100644
index 00000000..d28aaa99
--- /dev/null
+++ b/Kyoo.Authentication/Controllers/PasswordUtils.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+using IdentityModel;
+
+namespace Kyoo.Authentication
+{
+ public static class PasswordUtils
+ {
+ ///
+ /// Generate an OneTimeAccessCode.
+ ///
+ /// A new otac.
+ public static string GenerateOTAC()
+ {
+ return CryptoRandom.CreateUniqueId();
+ }
+
+ ///
+ /// Hash a password to store it has a verification only.
+ ///
+ /// The password to hash
+ /// The hashed password
+ public static string HashPassword(string password)
+ {
+ byte[] salt = new byte[16];
+ new RNGCryptoServiceProvider().GetBytes(salt);
+ Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
+ byte[] hash = pbkdf2.GetBytes(20);
+ byte[] hashBytes = new byte[36];
+ Array.Copy(salt, 0, hashBytes, 0, 16);
+ Array.Copy(hash, 0, hashBytes, 16, 20);
+ return Convert.ToBase64String(hashBytes);
+ }
+
+ ///
+ /// Check if a password is the same as a valid hashed password.
+ ///
+ /// The password to check
+ ///
+ /// The valid hashed password. This password must be hashed via .
+ ///
+ /// True if the password is valid, false otherwise.
+ public static bool CheckPassword(string password, string validPassword)
+ {
+ byte[] validHash = Convert.FromBase64String(validPassword);
+ byte[] salt = new byte[16];
+ Array.Copy(validHash, 0, salt, 0, 16);
+ Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
+ byte[] hash = pbkdf2.GetBytes(20);
+ return hash.SequenceEqual(validHash.Skip(16));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs
new file mode 100644
index 00000000..b844ce5f
--- /dev/null
+++ b/Kyoo.Authentication/Extensions.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using IdentityModel;
+using Kyoo.Models;
+
+namespace Kyoo.Authentication
+{
+ ///
+ /// Extension methods.
+ ///
+ public static class Extensions
+ {
+ public static ClaimsPrincipal ToPrincipal(this User user)
+ {
+ List claims = new()
+ {
+ new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
+ new Claim(JwtClaimTypes.Name, user.Username),
+ new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
+ };
+
+ ClaimsIdentity id = new (claims);
+ return new ClaimsPrincipal(id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj
index 7bb0a7fb..b334a943 100644
--- a/Kyoo.Authentication/Kyoo.Authentication.csproj
+++ b/Kyoo.Authentication/Kyoo.Authentication.csproj
@@ -19,6 +19,7 @@
+
diff --git a/Kyoo.Authentication/Models/DTO/LoginRequest.cs b/Kyoo.Authentication/Models/DTO/LoginRequest.cs
new file mode 100644
index 00000000..9bee4e04
--- /dev/null
+++ b/Kyoo.Authentication/Models/DTO/LoginRequest.cs
@@ -0,0 +1,28 @@
+namespace Kyoo.Authentication.Models.DTO
+{
+ ///
+ /// A model only used on login requests.
+ ///
+ public class LoginRequest
+ {
+ ///
+ /// The user's username.
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// The user's password.
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Should the user stay logged in? If true a cookie will be put.
+ ///
+ public bool StayLoggedIn { get; set; }
+
+ ///
+ /// The return url of the login flow.
+ ///
+ public string ReturnURL { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Models/DTO/OtacRequest.cs b/Kyoo.Authentication/Models/DTO/OtacRequest.cs
new file mode 100644
index 00000000..0c007f78
--- /dev/null
+++ b/Kyoo.Authentication/Models/DTO/OtacRequest.cs
@@ -0,0 +1,18 @@
+namespace Kyoo.Authentication.Models.DTO
+{
+ ///
+ /// A model to represent an otac request
+ ///
+ public class OtacRequest
+ {
+ ///
+ /// The One Time Access Code
+ ///
+ public string Otac { get; set; }
+
+ ///
+ /// Should the user stay logged
+ ///
+ public bool StayLoggedIn { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs b/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs
new file mode 100644
index 00000000..f35e22a3
--- /dev/null
+++ b/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs
@@ -0,0 +1,28 @@
+namespace Kyoo.Authentication.Models
+{
+ ///
+ /// The main authentication options.
+ ///
+ public class AuthenticationOptions
+ {
+ ///
+ /// The path to get this option from the root configuration.
+ ///
+ public const string Path = "authentication";
+
+ ///
+ /// The options for certificates
+ ///
+ public CertificateOption Certificate { get; set; }
+
+ ///
+ /// Options for permissions
+ ///
+ public PermissionOption Permissions { get; set; }
+
+ ///
+ /// Root path of user's profile pictures.
+ ///
+ public string ProfilePicturePath { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Models/CertificateOption.cs b/Kyoo.Authentication/Models/Options/CertificateOption.cs
similarity index 100%
rename from Kyoo.Authentication/Models/CertificateOption.cs
rename to Kyoo.Authentication/Models/Options/CertificateOption.cs
diff --git a/Kyoo.Authentication/Models/PermissionOption.cs b/Kyoo.Authentication/Models/Options/PermissionOption.cs
similarity index 83%
rename from Kyoo.Authentication/Models/PermissionOption.cs
rename to Kyoo.Authentication/Models/Options/PermissionOption.cs
index 772f3fb5..0a26f89e 100644
--- a/Kyoo.Authentication/Models/PermissionOption.cs
+++ b/Kyoo.Authentication/Models/Options/PermissionOption.cs
@@ -15,11 +15,11 @@ namespace Kyoo.Authentication.Models
///
/// The default permissions that will be given to a non-connected user.
///
- public ICollection Default { get; set; }
+ public string[] Default { get; set; }
///
/// Permissions applied to a new user.
///
- public ICollection NewUser { get; set; }
+ public string[] NewUser { get; set; }
}
}
\ No newline at end of file
diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs
index de47d57d..94cae0ad 100644
--- a/Kyoo.Authentication/Views/AccountApi.cs
+++ b/Kyoo.Authentication/Views/AccountApi.cs
@@ -1,183 +1,218 @@
-// using System;
-// using System.Collections.Generic;
-// using System.IO;
-// using System.Linq;
-// using System.Security.Claims;
-// using System.Threading.Tasks;
-// using IdentityServer4.Models;
-// using IdentityServer4.Services;
-// using Kyoo.Authentication.Models.DTO;
-// using Kyoo.Models;
-// using Microsoft.AspNetCore.Authorization;
-// using Microsoft.AspNetCore.Http;
-// using Microsoft.AspNetCore.Identity;
-// using Microsoft.AspNetCore.Mvc;
-// using Microsoft.Extensions.Configuration;
-// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
-//
-// namespace Kyoo.Authentication.Views
-// {
-// public class LoginRequest
-// {
-// public string Username { get; set; }
-// public string Password { get; set; }
-// public bool StayLoggedIn { get; set; }
-// }
-//
-// public class OtacRequest
-// {
-// public string Otac { get; set; }
-// public bool StayLoggedIn { get; set; }
-// }
-//
-// public class AccountData
-// {
-// [FromForm(Name = "email")]
-// public string Email { get; set; }
-// [FromForm(Name = "username")]
-// public string Username { get; set; }
-// [FromForm(Name = "picture")]
-// public IFormFile Picture { get; set; }
-// }
-//
-//
-// ///
-// /// The class responsible for login, logout, permissions and claims of a user.
-// ///
-// [Route("api/account")]
-// [Route("api/accounts")]
-// [ApiController]
-// public class AccountApi : Controller, IProfileService
-// {
-// private readonly UserManager _userManager;
-// private readonly SignInManager _signInManager;
-// private readonly IConfiguration _configuration;
-// private readonly string _picturePath;
-//
-// // TODO find how SignInManager & UserManager are implement and check if they can be used or not.
-// public AccountApi(UserManager userManager,
-// SignInManager signInManager,
-// IConfiguration configuration)
-// {
-// _userManager = userManager;
-// _signInManager = signInManager;
-// _picturePath = configuration.GetValue("profilePicturePath");
-// _configuration = configuration;
-// if (!Path.IsPathRooted(_picturePath))
-// _picturePath = Path.GetFullPath(_picturePath);
-// }
-//
-// [HttpPost("register")]
-// public async Task Register([FromBody] RegisterRequest request)
-// {
-// User user = request.ToUser();
-// IdentityResult result = await _userManager.CreateAsync(user, user.Password);
-// if (!result.Succeeded)
-// return BadRequest(result.Errors);
-// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
-// await _userManager.UpdateAsync(account);
-// await _userManager.AddClaimAsync(account, new Claim(
-// "permissions",
-// _configuration.GetValue("newUserPermissions")));
-// return Ok(new {otac});
-// }
-//
-// [HttpPost("login")]
-// public async Task Login([FromBody] LoginRequest login)
-// {
-// if (!ModelState.IsValid)
-// return BadRequest(login);
-// SignInResult result = await _signInManager
-// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false);
-// if (result.Succeeded)
-// return Ok();
-// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}});
-// }
-//
-// [HttpPost("otac-login")]
-// public async Task OtacLogin([FromBody] OtacRequest otac)
-// {
-// if (!ModelState.IsValid)
-// return BadRequest(otac);
-// User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac);
-// if (user == null)
-// return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}});
-// if (user.OTACExpires <= DateTime.UtcNow)
-// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
-// await _signInManager.SignInAsync(user, otac.StayLoggedIn);
-// return Ok();
-// }
-//
-// [HttpGet("logout")]
-// [Authorize]
-// public async Task Logout()
-// {
-// await _signInManager.SignOutAsync();
-// return Ok();
-// }
-//
-// public async Task GetProfileDataAsync(ProfileDataRequestContext context)
-// {
-// User user = await _userManager.GetUserAsync(context.Subject);
-// if (user != null)
-// {
-// List claims = new()
-// {
-// new Claim("email", user.Email),
-// new Claim("username", user.UserName),
-// new Claim("picture", $"api/account/picture/{user.UserName}")
-// };
-//
-// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
-// if (perms != null)
-// claims.Add(perms);
-//
-// context.IssuedClaims.AddRange(claims);
-// }
-// }
-//
-// public async Task IsActiveAsync(IsActiveContext context)
-// {
-// User user = await _userManager.GetUserAsync(context.Subject);
-// context.IsActive = user != null;
-// }
-//
-// [HttpGet("picture/{username}")]
-// public async Task GetPicture(string username)
-// {
-// User user = await _userManager.FindByNameAsync(username);
-// if (user == null)
-// return BadRequest();
-// string path = Path.Combine(_picturePath, user.Id);
-// if (!System.IO.File.Exists(path))
-// return NotFound();
-// return new PhysicalFileResult(path, "image/png");
-// }
-//
-// [HttpPost("update")]
-// [Authorize]
-// public async Task Update([FromForm] AccountData data)
-// {
-// User user = await _userManager.GetUserAsync(HttpContext.User);
-//
-// if (!string.IsNullOrEmpty(data.Email))
-// user.Email = data.Email;
-// if (!string.IsNullOrEmpty(data.Username))
-// user.UserName = data.Username;
-// if (data.Picture?.Length > 0)
-// {
-// string path = Path.Combine(_picturePath, user.Id);
-// await using FileStream file = System.IO.File.Create(path);
-// await data.Picture.CopyToAsync(file);
-// }
-// await _userManager.UpdateAsync(user);
-// return Ok();
-// }
-//
-// [HttpGet("default-permissions")]
-// public ActionResult> GetDefaultPermissions()
-// {
-// return _configuration.GetValue("defaultPermissions").Split(",");
-// }
-// }
-// }
\ No newline at end of file
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using IdentityModel;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using Kyoo.Authentication.Models.DTO;
+using Kyoo.Controllers;
+using Kyoo.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
+
+namespace Kyoo.Authentication.Views
+{
+ public class AccountData
+ {
+ [FromForm(Name = "email")]
+ public string Email { get; set; }
+ [FromForm(Name = "username")]
+ public string Username { get; set; }
+ [FromForm(Name = "picture")]
+ public IFormFile Picture { get; set; }
+ }
+
+
+ ///
+ /// The class responsible for login, logout, permissions and claims of a user.
+ ///
+ [Route("api/account")]
+ [Route("api/accounts")]
+ [ApiController]
+ public class AccountApi : Controller, IProfileService
+ {
+ ///
+ /// The repository to handle users.
+ ///
+ private readonly IUserRepository _users;
+ ///
+ /// The identity server interaction service to login users.
+ ///
+ private readonly IIdentityServerInteractionService _interaction;
+ ///
+ /// Options about authentication. Those options are monitored and reloads are supported.
+ ///
+ private readonly IOptionsMonitor _options;
+
+
+ ///
+ /// Create a new handle to handle login/users requests.
+ ///
+ /// The user repository to create and manage users
+ /// The identity server interaction service to login users.
+ /// Authentication options (this may be hot reloaded)
+ public AccountApi(IUserRepository users,
+ IIdentityServerInteractionService interaction,
+ IOptionsMonitor options)
+ {
+ _users = users;
+ _interaction = interaction;
+ _options = options;
+ }
+
+
+ ///
+ /// Register a new user and return a OTAC to connect to it.
+ ///
+ /// The DTO register request
+ /// A OTAC to connect to this new account
+ [HttpPost("register")]
+ public async Task> Register([FromBody] RegisterRequest request)
+ {
+ User user = request.ToUser();
+ user.Permissions = _options.CurrentValue.Permissions.NewUser;
+ user.Password = PasswordUtils.HashPassword(user.Password);
+ user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
+ user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
+ await _users.Create(user);
+ return user.ExtraData["otac"];
+ }
+
+ ///
+ /// Return an authentication properties based on a stay login property
+ ///
+ /// Should the user stay logged
+ /// Authentication properties based on a stay login
+ private static AuthenticationProperties StayLogged(bool stayLogged)
+ {
+ if (!stayLogged)
+ return null;
+ return new AuthenticationProperties
+ {
+ IsPersistent = true,
+ ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
+ };
+ }
+
+ ///
+ /// Login the user.
+ ///
+ /// The DTO login request
+ [HttpPost("login")]
+ public async Task Login([FromBody] LoginRequest login)
+ {
+ AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL);
+ User user = await _users.Get(x => x.Username == login.Username);
+
+ if (context == null || user == null)
+ return Unauthorized();
+ if (!PasswordUtils.CheckPassword(login.Password, user.Password))
+ return Unauthorized();
+
+ await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(login.StayLoggedIn));
+ return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
+ }
+
+ ///
+ /// Use a OTAC to login a user.
+ ///
+ /// The OTAC request
+ [HttpPost("otac-login")]
+ public async Task OtacLogin([FromBody] OtacRequest otac)
+ {
+ User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac);
+ if (user == null)
+ return Unauthorized();
+ if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow)
+ return BadRequest(new
+ {
+ code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
+ });
+ await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(otac.StayLoggedIn));
+ return Ok();
+ }
+
+ ///
+ /// Sign out an user
+ ///
+ [HttpGet("logout")]
+ [Authorize]
+ public async Task Logout()
+ {
+ await HttpContext.SignOutAsync();
+ return Ok();
+ }
+
+ // TODO check with the extension method
+ public async Task GetProfileDataAsync(ProfileDataRequestContext context)
+ {
+ User user = await _userManager.GetUserAsync(context.Subject);
+ if (user != null)
+ {
+ List claims = new()
+ {
+ new Claim(JwtClaimTypes.Email, user.Email),
+ new Claim(JwtClaimTypes.Name, user.Username),
+ new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
+ };
+
+ Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
+ if (perms != null)
+ claims.Add(perms);
+
+ context.IssuedClaims.AddRange(claims);
+ }
+ }
+
+ public async Task IsActiveAsync(IsActiveContext context)
+ {
+ User user = await _userManager.GetUserAsync(context.Subject);
+ context.IsActive = user != null;
+ }
+
+ [HttpGet("picture/{username}")]
+ public async Task GetPicture(string username)
+ {
+ User user = await _userManager.FindByNameAsync(username);
+ if (user == null)
+ return BadRequest();
+ string path = Path.Combine(_picturePath, user.Id);
+ if (!System.IO.File.Exists(path))
+ return NotFound();
+ return new PhysicalFileResult(path, "image/png");
+ }
+
+ [HttpPost("update")]
+ [Authorize]
+ public async Task Update([FromForm] AccountData data)
+ {
+ User user = await _userManager.GetUserAsync(HttpContext.User);
+
+ if (!string.IsNullOrEmpty(data.Email))
+ user.Email = data.Email;
+ if (!string.IsNullOrEmpty(data.Username))
+ user.UserName = data.Username;
+ if (data.Picture?.Length > 0)
+ {
+ string path = Path.Combine(_picturePath, user.Id);
+ await using FileStream file = System.IO.File.Create(path);
+ await data.Picture.CopyToAsync(file);
+ }
+ await _userManager.UpdateAsync(user);
+ return Ok();
+ }
+
+ [HttpGet("default-permissions")]
+ public ActionResult> GetDefaultPermissions()
+ {
+ return _configuration.GetValue("defaultPermissions").Split(",");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs
index bc5ed0bc..4e8b16a3 100644
--- a/Kyoo.Common/Controllers/IRepository.cs
+++ b/Kyoo.Common/Controllers/IRepository.cs
@@ -11,7 +11,7 @@ using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
{
///
- /// Informations about the pagination. How many items should be displayed and where to start.
+ /// Information about the pagination. How many items should be displayed and where to start.
///
public readonly struct Pagination
{
@@ -44,7 +44,7 @@ namespace Kyoo.Controllers
}
///
- /// Informations about how a query should be sorted. What factor should decide the sort and in which order.
+ /// Information about how a query should be sorted. What factor should decide the sort and in which order.
///
/// For witch type this sort applies
public readonly struct Sort
@@ -54,7 +54,7 @@ namespace Kyoo.Controllers
///
public Expression> Key { get; }
///
- /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendent order.
+ /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
///
public bool Descendant { get; }
@@ -175,7 +175,7 @@ namespace Kyoo.Controllers
/// Get every resources that match all filters
///
/// A filter predicate
- /// Sort informations about the query (sort by, sort order)
+ /// Sort information about the query (sort by, sort order)
/// How pagination should be done (where to start and how many to return)
/// A list of resources that match every filters
Task> GetAll(Expression> where = null,
@@ -205,24 +205,24 @@ namespace Kyoo.Controllers
/// Create a new resource.
///
/// The item to register
- /// The resource registers and completed by database's informations (related items & so on)
+ /// The resource registers and completed by database's information (related items & so on)
Task Create([NotNull] T obj);
///
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
///
/// The object to create
- /// Allow issues to occurs in this method. Every issue is catched and ignored.
+ /// Allow issues to occurs in this method. Every issue is caught and ignored.
/// The newly created item or the existing value if it existed.
Task CreateIfNotExists([NotNull] T obj, bool silentFail = false);
///
/// Edit a resource
///
- /// The resourcce to edit, it's ID can't change.
+ /// The resource to edit, it's ID can't change.
/// Should old properties of the resource be discarded or should null values considered as not changed?
/// If the item is not found
- /// The resource edited and completed by database's informations (related items & so on)
+ /// The resource edited and completed by database's information (related items & so on)
Task Edit([NotNull] T edited, bool resetOld);
///
@@ -259,25 +259,25 @@ namespace Kyoo.Controllers
///
/// Delete a list of resources.
///
- /// One or multiple resources's id
+ /// One or multiple resource's id
/// If the item is not found
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
///
/// Delete a list of resources.
///
- /// An enumearble of resources's id
+ /// An enumerable of resource's id
/// If the item is not found
Task DeleteRange(IEnumerable ids);
///
/// Delete a list of resources.
///
- /// One or multiple resources's slug
+ /// One or multiple resource's slug
/// If the item is not found
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
///
/// Delete a list of resources.
///
- /// An enumerable of resources's slug
+ /// An enumerable of resource's slug
/// If the item is not found
Task DeleteRange(IEnumerable slugs);
///
@@ -294,7 +294,7 @@ namespace Kyoo.Controllers
public interface IShowRepository : IRepository
{
///
- /// Link a show to a collection and/or a library. The given show is now part of thoses containers.
+ /// Link a show to a collection and/or a library. The given show is now part of those containers.
/// If both a library and a collection are given, the collection is added to the library too.
///
/// The ID of the show
@@ -421,7 +421,7 @@ namespace Kyoo.Controllers
/// The slug of the track
/// The type (Video, Audio or Subtitle)
/// If the item is not found
- /// The tracl found
+ /// The track found
Task
/// The ID of the show
/// A filter function
- /// Sort informations (sort order & sort by)
+ /// Sort information (sort order & sort by)
/// How many items to return and where to start
/// A list of items that match every filters
Task> GetFromShow(int showID,
@@ -547,7 +547,7 @@ namespace Kyoo.Controllers
///
/// The slug of the show
/// A filter function
- /// Sort informations (sort order & sort by)
+ /// Sort information (sort order & sort by)
/// How many items to return and where to start
/// A list of items that match every filters
Task> GetFromShow(string showSlug,
@@ -573,7 +573,7 @@ namespace Kyoo.Controllers
///
/// The id of the person
/// A filter function
- /// Sort informations (sort order & sort by)
+ /// Sort information (sort order & sort by)
/// How many items to return and where to start
/// A list of items that match every filters
Task> GetFromPeople(int id,
@@ -599,7 +599,7 @@ namespace Kyoo.Controllers
///
/// The slug of the person
/// A filter function
- /// Sort informations (sort order & sort by)
+ /// Sort information (sort order & sort by)
/// How many items to return and where to start
/// A list of items that match every filters
Task> GetFromPeople(string slug,
@@ -631,7 +631,7 @@ namespace Kyoo.Controllers
///
/// A predicate to add arbitrary filter
/// Sort information (sort order & sort by)
- /// Paginations information (where to start and how many to get)
+ /// Pagination information (where to start and how many to get)
/// A filtered list of external ids.
Task> GetMetadataID(Expression> where = null,
Sort sort = default,
@@ -642,11 +642,16 @@ namespace Kyoo.Controllers
///
/// A predicate to add arbitrary filter
/// A sort by expression
- /// Paginations information (where to start and how many to get)
+ /// Pagination information (where to start and how many to get)
/// A filtered list of external ids.
Task> GetMetadataID([Optional] Expression> where,
Expression> sort,
Pagination limit = default
) => GetMetadataID(where, new Sort(sort), limit);
}
+
+ ///
+ /// A repository to handle users.
+ ///
+ public interface IUserRepository : IRepository {}
}
diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs
index a4367c11..da85af0b 100644
--- a/Kyoo.Common/Models/Resources/User.cs
+++ b/Kyoo.Common/Models/Resources/User.cs
@@ -28,6 +28,16 @@ namespace Kyoo.Models
///
public string Password { get; set; }
+ ///
+ /// The list of permissions of the user. The format of this is implementation dependent.
+ ///
+ public string[] Permissions { get; set; }
+
+ ///
+ /// Arbitrary extra data that can be used by specific authentication implementations.
+ ///
+ public Dictionary ExtraData { get; set; }
+
///
/// The list of shows the user has finished.
///
@@ -36,6 +46,18 @@ namespace Kyoo.Models
///
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
///
- public ICollection<(Episode episode, int watchedPercentage)> CurrentlyWatching { get; set; }
+ public ICollection CurrentlyWatching { get; set; }
+
+#if ENABLE_INTERNAL_LINKS
+ ///
+ /// Links between Users and Shows.
+ ///
+ public ICollection> ShowLinks { get; set; }
+
+ ///
+ /// Links between Users and WatchedEpisodes.
+ ///
+ public ICollection> EpisodeLinks { get; set; }
+#endif
}
}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/WatchedEpisode.cs b/Kyoo.Common/Models/WatchedEpisode.cs
new file mode 100644
index 00000000..631c7572
--- /dev/null
+++ b/Kyoo.Common/Models/WatchedEpisode.cs
@@ -0,0 +1,30 @@
+using Kyoo.Models.Attributes;
+
+namespace Kyoo.Models
+{
+ ///
+ /// Metadata of episode currently watching by an user
+ ///
+ public class WatchedEpisode : IResource
+ {
+ ///
+ [SerializeIgnore] public int ID
+ {
+ get => Episode.ID;
+ set => Episode.ID = value;
+ }
+
+ ///
+ [SerializeIgnore] public string Slug => Episode.Slug;
+
+ ///
+ /// The episode currently watched
+ ///
+ public Episode Episode { get; set; }
+
+ ///
+ /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
+ ///
+ public int WatchedPercentage { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs
index ff44761a..6583f704 100644
--- a/Kyoo.Common/Utility.cs
+++ b/Kyoo.Common/Utility.cs
@@ -215,7 +215,7 @@ namespace Kyoo
///
/// An advanced function.
/// This will set missing values of to the corresponding values of .
- /// Enumerables will be merged (concatened).
+ /// Enumerable will be merged (concatenated).
/// At the end, the OnMerge method of first will be called if first is a .
///
/// The object to complete
diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs
index a795723e..4a4b416b 100644
--- a/Kyoo.CommonAPI/DatabaseContext.cs
+++ b/Kyoo.CommonAPI/DatabaseContext.cs
@@ -67,7 +67,7 @@ namespace Kyoo
///
/// The list of registered users.
///
- // public DbSet Users { get; set; }
+ public DbSet Users { get; set; }
///
/// All people's role. See .
@@ -254,7 +254,7 @@ namespace Kyoo
/// Return a new or an in cache temporary object wih the same ID as the one given
///
/// If a resource with the same ID is found in the database, it will be used.
- /// will be used overwise
+ /// will be used otherwise
/// The type of the resource
/// A resource that is now tracked by this context.
public T GetTemporaryObject(T model)
diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs
index 32b47bd2..ee01b089 100644
--- a/Kyoo.CommonAPI/LocalRepository.cs
+++ b/Kyoo.CommonAPI/LocalRepository.cs
@@ -118,7 +118,7 @@ namespace Kyoo.Controllers
/// The base query to filter.
/// An expression to filter based on arbitrary conditions
/// The sort settings (sort order & sort by)
- /// Paginations information (where to start and how many to get)
+ /// Pagination information (where to start and how many to get)
/// The filtered query
protected Task> ApplyFilters(IQueryable query,
Expression> where = null,
@@ -137,7 +137,7 @@ namespace Kyoo.Controllers
/// The base query to filter.
/// An expression to filter based on arbitrary conditions
/// The sort settings (sort order & sort by)
- /// Paginations information (where to start and how many to get)
+ /// Pagination information (where to start and how many to get)
/// The filtered query
protected async Task> ApplyFilters(IQueryable query,
Func> get,
@@ -244,7 +244,7 @@ namespace Kyoo.Controllers
}
///
- /// An overridable method to edit relatiosn of a resource.
+ /// An overridable method to edit relation of a resource.
///
/// The non edited resource
/// The new version of . This item will be saved on the databse and replace
diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs
index d025c31c..61f41593 100644
--- a/Kyoo/Controllers/ProviderManager.cs
+++ b/Kyoo/Controllers/ProviderManager.cs
@@ -33,7 +33,7 @@ namespace Kyoo.Controllers
} catch (Exception ex)
{
await Console.Error.WriteLineAsync(
- $"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
+ $"The provider {provider.Provider.Name} could not work for {what}. Exception: {ex.Message}");
}
}
return ret;
diff --git a/Kyoo/Controllers/Repositories/UserRepository.cs b/Kyoo/Controllers/Repositories/UserRepository.cs
new file mode 100644
index 00000000..97bce050
--- /dev/null
+++ b/Kyoo/Controllers/Repositories/UserRepository.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Kyoo.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Kyoo.Controllers
+{
+ ///
+ /// A repository for users.
+ ///
+ public class UserRepository : LocalRepository, IUserRepository
+ {
+ ///
+ /// The database handle
+ ///
+ private readonly DatabaseContext _database;
+
+ ///
+ protected override Expression> DefaultSort => x => x.Username;
+
+
+ ///
+ /// Create a new
+ ///
+ /// The database handle to use
+ public UserRepository(DatabaseContext database)
+ : base(database)
+ {
+ _database = database;
+ }
+
+ ///
+ public override async Task> Search(string query)
+ {
+ return await _database.Users
+ .Where(_database.Like(x => x.Username, $"%{query}%"))
+ .OrderBy(DefaultSort)
+ .Take(20)
+ .ToListAsync();
+ }
+
+ ///
+ public override async Task Delete(User obj)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+
+ _database.Entry(obj).State = EntityState.Deleted;
+ await _database.SaveChangesAsync();
+ }
+ }
+}
\ No newline at end of file