mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Reworking login requests
This commit is contained in:
parent
0f2bea9bc4
commit
d9cca97961
54
Kyoo.Authentication/Controllers/PasswordUtils.cs
Normal file
54
Kyoo.Authentication/Controllers/PasswordUtils.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
26
Kyoo.Authentication/Extensions.cs
Normal file
26
Kyoo.Authentication/Extensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||
<PackageReference Include="IdentityServer4.Storage" Version="4.1.2" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
|
28
Kyoo.Authentication/Models/DTO/LoginRequest.cs
Normal file
28
Kyoo.Authentication/Models/DTO/LoginRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
18
Kyoo.Authentication/Models/DTO/OtacRequest.cs
Normal file
18
Kyoo.Authentication/Models/DTO/OtacRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
28
Kyoo.Authentication/Models/Options/AuthenticationOptions.cs
Normal file
28
Kyoo.Authentication/Models/Options/AuthenticationOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -15,11 +15,11 @@ namespace Kyoo.Authentication.Models
|
||||
/// <summary>
|
||||
/// The default permissions that will be given to a non-connected user.
|
||||
/// </summary>
|
||||
public ICollection<string> Default { get; set; }
|
||||
public string[] Default { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Permissions applied to a new user.
|
||||
/// </summary>
|
||||
public ICollection<string> NewUser { get; set; }
|
||||
public string[] NewUser { get; set; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /// <summary>
|
||||
// /// The class responsible for login, logout, permissions and claims of a user.
|
||||
// /// </summary>
|
||||
// [Route("api/account")]
|
||||
// [Route("api/accounts")]
|
||||
// [ApiController]
|
||||
// public class AccountApi : Controller, IProfileService
|
||||
// {
|
||||
// private readonly UserManager<User> _userManager;
|
||||
// private readonly SignInManager<User> _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<User> userManager,
|
||||
// SignInManager<User> signInManager,
|
||||
// IConfiguration configuration)
|
||||
// {
|
||||
// _userManager = userManager;
|
||||
// _signInManager = signInManager;
|
||||
// _picturePath = configuration.GetValue<string>("profilePicturePath");
|
||||
// _configuration = configuration;
|
||||
// if (!Path.IsPathRooted(_picturePath))
|
||||
// _picturePath = Path.GetFullPath(_picturePath);
|
||||
// }
|
||||
//
|
||||
// [HttpPost("register")]
|
||||
// public async Task<IActionResult> 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<string>("newUserPermissions")));
|
||||
// return Ok(new {otac});
|
||||
// }
|
||||
//
|
||||
// [HttpPost("login")]
|
||||
// public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Logout()
|
||||
// {
|
||||
// await _signInManager.SignOutAsync();
|
||||
// return Ok();
|
||||
// }
|
||||
//
|
||||
// public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
// {
|
||||
// User user = await _userManager.GetUserAsync(context.Subject);
|
||||
// if (user != null)
|
||||
// {
|
||||
// List<Claim> 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<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(",");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The class responsible for login, logout, permissions and claims of a user.
|
||||
/// </summary>
|
||||
[Route("api/account")]
|
||||
[Route("api/accounts")]
|
||||
[ApiController]
|
||||
public class AccountApi : Controller, IProfileService
|
||||
{
|
||||
/// <summary>
|
||||
/// The repository to handle users.
|
||||
/// </summary>
|
||||
private readonly IUserRepository _users;
|
||||
/// <summary>
|
||||
/// The identity server interaction service to login users.
|
||||
/// </summary>
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
/// <summary>
|
||||
/// Options about authentication. Those options are monitored and reloads are supported.
|
||||
/// </summary>
|
||||
private readonly IOptionsMonitor<AuthenticationOptions> _options;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
|
||||
/// </summary>
|
||||
/// <param name="users">The user repository to create and manage users</param>
|
||||
/// <param name="interaction">The identity server interaction service to login users.</param>
|
||||
/// <param name="options">Authentication options (this may be hot reloaded)</param>
|
||||
public AccountApi(IUserRepository users,
|
||||
IIdentityServerInteractionService interaction,
|
||||
IOptionsMonitor<AuthenticationOptions> options)
|
||||
{
|
||||
_users = users;
|
||||
_interaction = interaction;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Register a new user and return a OTAC to connect to it.
|
||||
/// </summary>
|
||||
/// <param name="request">The DTO register request</param>
|
||||
/// <returns>A OTAC to connect to this new account</returns>
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<string>> 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"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an authentication properties based on a stay login property
|
||||
/// </summary>
|
||||
/// <param name="stayLogged">Should the user stay logged</param>
|
||||
/// <returns>Authentication properties based on a stay login</returns>
|
||||
private static AuthenticationProperties StayLogged(bool stayLogged)
|
||||
{
|
||||
if (!stayLogged)
|
||||
return null;
|
||||
return new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Login the user.
|
||||
/// </summary>
|
||||
/// <param name="login">The DTO login request</param>
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> 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 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use a OTAC to login a user.
|
||||
/// </summary>
|
||||
/// <param name="otac">The OTAC request</param>
|
||||
[HttpPost("otac-login")]
|
||||
public async Task<IActionResult> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign out an user
|
||||
/// </summary>
|
||||
[HttpGet("logout")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> 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<Claim> 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<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(",");
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using Kyoo.Models.Exceptions;
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <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>
|
||||
public readonly struct Pagination
|
||||
{
|
||||
@ -44,7 +44,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <typeparam name="T">For witch type this sort applies</typeparam>
|
||||
public readonly struct Sort<T>
|
||||
@ -54,7 +54,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
public Expression<Func<T, object>> Key { get; }
|
||||
/// <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>
|
||||
public bool Descendant { get; }
|
||||
|
||||
@ -175,7 +175,7 @@ namespace Kyoo.Controllers
|
||||
/// Get every resources that match all filters
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A list of resources that match every filters</returns>
|
||||
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
|
||||
@ -205,24 +205,24 @@ namespace Kyoo.Controllers
|
||||
/// Create a new resource.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false);
|
||||
|
||||
/// <summary>
|
||||
/// Edit a resource
|
||||
/// </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>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
@ -259,25 +259,25 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </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>
|
||||
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </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>
|
||||
Task DeleteRange(IEnumerable<int> ids);
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </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>
|
||||
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </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>
|
||||
Task DeleteRange(IEnumerable<string> slugs);
|
||||
/// <summary>
|
||||
@ -294,7 +294,7 @@ namespace Kyoo.Controllers
|
||||
public interface IShowRepository : IRepository<Show>
|
||||
{
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <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="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
@ -429,7 +429,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the track</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);
|
||||
}
|
||||
|
||||
@ -439,16 +439,16 @@ namespace Kyoo.Controllers
|
||||
public interface ILibraryRepository : IRepository<Library> { }
|
||||
|
||||
/// <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>
|
||||
public interface ILibraryItemRepository : IRepository<LibraryItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get items (A wrapper arround shows or collections) from a library.
|
||||
/// Get items (A wrapper around shows or collections) from a library.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the library</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
|
||||
@ -456,7 +456,7 @@ namespace Kyoo.Controllers
|
||||
Sort<LibraryItem> sort = default,
|
||||
Pagination limit = default);
|
||||
/// <summary>
|
||||
/// Get items (A wrapper arround shows or collections) from a library.
|
||||
/// Get items (A wrapper around shows or collections) from a library.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the library</param>
|
||||
/// <param name="where">A filter function</param>
|
||||
@ -470,11 +470,11 @@ namespace Kyoo.Controllers
|
||||
) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
|
||||
|
||||
/// <summary>
|
||||
/// Get items (A wrapper arround shows or collections) from a library.
|
||||
/// Get items (A wrapper around shows or collections) from a library.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the library</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
|
||||
@ -482,7 +482,7 @@ namespace Kyoo.Controllers
|
||||
Sort<LibraryItem> sort = default,
|
||||
Pagination limit = default);
|
||||
/// <summary>
|
||||
/// Get items (A wrapper arround shows or collections) from a library.
|
||||
/// Get items (A wrapper around shows or collections) from a library.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the library</param>
|
||||
/// <param name="where">A filter function</param>
|
||||
@ -521,7 +521,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="showID">The ID of the show</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
Task<ICollection<PeopleRole>> GetFromShow(int showID,
|
||||
@ -547,7 +547,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="showSlug">The slug of the show</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
|
||||
@ -573,7 +573,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the person</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
Task<ICollection<PeopleRole>> GetFromPeople(int id,
|
||||
@ -599,7 +599,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the person</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>
|
||||
/// <returns>A list of items that match every filters</returns>
|
||||
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
|
||||
@ -631,7 +631,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="where">A predicate to add arbitrary filter</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>
|
||||
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
|
||||
Sort<MetadataID> sort = default,
|
||||
@ -642,11 +642,16 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="where">A predicate to add arbitrary filter</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>
|
||||
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
|
||||
Expression<Func<MetadataID, object>> sort,
|
||||
Pagination limit = default
|
||||
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A repository to handle users.
|
||||
/// </summary>
|
||||
public interface IUserRepository : IRepository<User> {}
|
||||
}
|
||||
|
@ -28,6 +28,16 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
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>
|
||||
/// The list of shows the user has finished.
|
||||
/// </summary>
|
||||
@ -36,6 +46,18 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
|
||||
/// </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
|
||||
}
|
||||
}
|
30
Kyoo.Common/Models/WatchedEpisode.cs
Normal file
30
Kyoo.Common/Models/WatchedEpisode.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -215,7 +215,7 @@ namespace Kyoo
|
||||
/// <summary>
|
||||
/// An advanced <see cref="Complete{T}"/> function.
|
||||
/// 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"/>.
|
||||
/// </summary>
|
||||
/// <param name="first">The object to complete</param>
|
||||
|
@ -67,7 +67,7 @@ namespace Kyoo
|
||||
/// <summary>
|
||||
/// The list of registered users.
|
||||
/// </summary>
|
||||
// public DbSet<User> Users { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A resource that is now tracked by this context.</returns>
|
||||
public T GetTemporaryObject<T>(T model)
|
||||
|
@ -118,7 +118,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="query">The base query to filter.</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="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>
|
||||
protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
|
||||
Expression<Func<T, bool>> where = null,
|
||||
@ -137,7 +137,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="query">The base query to filter.</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="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>
|
||||
protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query,
|
||||
Func<int, Task<TValue>> get,
|
||||
@ -244,7 +244,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An overridable method to edit relatiosn of a resource.
|
||||
/// An overridable method to edit relation of a resource.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
|
@ -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;
|
||||
|
55
Kyoo/Controllers/Repositories/UserRepository.cs
Normal file
55
Kyoo/Controllers/Repositories/UserRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user