mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -04:00
Starting to rework authentication
This commit is contained in:
parent
da699c096d
commit
0f2bea9bc4
@ -64,7 +64,7 @@ namespace Kyoo.Authentication
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||||
{
|
{
|
||||||
string publicUrl = _configuration.GetValue<string>("public_url");
|
string publicUrl = _configuration.GetValue<string>("public_url").TrimEnd('/');
|
||||||
|
|
||||||
// services.AddDbContext<IdentityDatabase>(options =>
|
// services.AddDbContext<IdentityDatabase>(options =>
|
||||||
// {
|
// {
|
||||||
@ -86,9 +86,9 @@ namespace Kyoo.Authentication
|
|||||||
services.AddIdentityServer(options =>
|
services.AddIdentityServer(options =>
|
||||||
{
|
{
|
||||||
options.IssuerUri = publicUrl;
|
options.IssuerUri = publicUrl;
|
||||||
options.UserInteraction.LoginUrl = publicUrl + "login";
|
options.UserInteraction.LoginUrl = $"{publicUrl}/login";
|
||||||
options.UserInteraction.ErrorUrl = publicUrl + "error";
|
options.UserInteraction.ErrorUrl = $"{publicUrl}/error";
|
||||||
options.UserInteraction.LogoutUrl = publicUrl + "logout";
|
options.UserInteraction.LogoutUrl = $"{publicUrl}/logout";
|
||||||
})
|
})
|
||||||
// .AddAspNetIdentity<User>()
|
// .AddAspNetIdentity<User>()
|
||||||
// .AddConfigurationStore(options =>
|
// .AddConfigurationStore(options =>
|
||||||
@ -105,11 +105,13 @@ namespace Kyoo.Authentication
|
|||||||
// options.EnableTokenCleanup = true;
|
// options.EnableTokenCleanup = true;
|
||||||
// })
|
// })
|
||||||
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
|
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
|
||||||
.AddInMemoryApiScopes(IdentityContext.GetScopes())
|
|
||||||
.AddInMemoryApiResources(IdentityContext.GetApis())
|
.AddInMemoryApiResources(IdentityContext.GetApis())
|
||||||
.AddInMemoryClients(IdentityContext.GetClients())
|
.AddInMemoryClients(IdentityContext.GetClients())
|
||||||
// .AddProfileService<AccountController>()
|
.AddDeveloperSigningCredential();
|
||||||
.AddSigninKeys(certificateOptions);
|
// .AddProfileService<AccountApi>()
|
||||||
|
// .AddSigninKeys(certificateOptions);
|
||||||
|
// TODO implement means to add clients or api scopes for other plugins.
|
||||||
|
// TODO split scopes (kyoo.read should be task.read, video.read etc)
|
||||||
|
|
||||||
services.AddAuthentication(o =>
|
services.AddAuthentication(o =>
|
||||||
{
|
{
|
||||||
|
@ -17,9 +17,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||||
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
|
|
||||||
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
|
|
||||||
<PackageReference Include="IdentityServer4.EntityFramework.Storage" 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="Portable.BouncyCastle" Version="1.8.10" />
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
|
||||||
|
45
Kyoo.Authentication/Models/DTO/RegisterRequest.cs
Normal file
45
Kyoo.Authentication/Models/DTO/RegisterRequest.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Kyoo.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models.DTO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A model only used on register requests.
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user email address
|
||||||
|
/// </summary>
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's username.
|
||||||
|
/// </summary>
|
||||||
|
[MinLength(4)]
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's password.
|
||||||
|
/// </summary>
|
||||||
|
[MinLength(8)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert this register request to a new <see cref="User"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public User ToUser()
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(Username),
|
||||||
|
Username = Username,
|
||||||
|
Password = Password,
|
||||||
|
Email = Email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,22 @@
|
|||||||
using System;
|
// using System;
|
||||||
using IdentityModel;
|
// using IdentityModel;
|
||||||
using Microsoft.AspNetCore.Identity;
|
// using Microsoft.AspNetCore.Identity;
|
||||||
|
//
|
||||||
namespace Kyoo.Models
|
// namespace Kyoo.Models
|
||||||
{
|
// {
|
||||||
public class User : IdentityUser
|
// public class User : IdentityUser
|
||||||
{
|
// {
|
||||||
public string OTAC { get; set; }
|
// public string OTAC { get; set; }
|
||||||
public DateTime? OTACExpires { get; set; }
|
// public DateTime? OTACExpires { get; set; }
|
||||||
|
//
|
||||||
public string GenerateOTAC(TimeSpan validFor)
|
// public string GenerateOTAC(TimeSpan validFor)
|
||||||
{
|
// {
|
||||||
string otac = CryptoRandom.CreateUniqueId();
|
// string otac = CryptoRandom.CreateUniqueId();
|
||||||
string hashed = otac; // TODO should add a good hashing here.
|
// string hashed = otac; // TODO should add a good hashing here.
|
||||||
|
//
|
||||||
OTAC = hashed;
|
// OTAC = hashed;
|
||||||
OTACExpires = DateTime.UtcNow.Add(validFor);
|
// OTACExpires = DateTime.UtcNow.Add(validFor);
|
||||||
return otac;
|
// return otac;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
@ -1,191 +1,183 @@
|
|||||||
using System;
|
// using System;
|
||||||
using System.Collections.Generic;
|
// using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
// using System.IO;
|
||||||
using System.IO;
|
// using System.Linq;
|
||||||
using System.Linq;
|
// using System.Security.Claims;
|
||||||
using System.Security.Claims;
|
// using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
// using IdentityServer4.Models;
|
||||||
using IdentityServer4.Models;
|
// using IdentityServer4.Services;
|
||||||
using IdentityServer4.Services;
|
// using Kyoo.Authentication.Models.DTO;
|
||||||
using Kyoo.Models;
|
// using Kyoo.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
// using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
// using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
// using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
// using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
// using Microsoft.Extensions.Configuration;
|
||||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||||
|
//
|
||||||
namespace Kyoo.Api
|
// namespace Kyoo.Authentication.Views
|
||||||
{
|
// {
|
||||||
public class RegisterRequest
|
// public class LoginRequest
|
||||||
{
|
// {
|
||||||
public string Email { get; set; }
|
// public string Username { get; set; }
|
||||||
public string Username { get; set; }
|
// public string Password { get; set; }
|
||||||
public string Password { get; set; }
|
// public bool StayLoggedIn { get; set; }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public class LoginRequest
|
// public class OtacRequest
|
||||||
{
|
// {
|
||||||
public string Username { get; set; }
|
// public string Otac { get; set; }
|
||||||
public string Password { get; set; }
|
// public bool StayLoggedIn { get; set; }
|
||||||
public bool StayLoggedIn { get; set; }
|
// }
|
||||||
}
|
//
|
||||||
|
// public class AccountData
|
||||||
public class OtacRequest
|
// {
|
||||||
{
|
// [FromForm(Name = "email")]
|
||||||
public string Otac { get; set; }
|
// public string Email { get; set; }
|
||||||
public bool StayLoggedIn { get; set; }
|
// [FromForm(Name = "username")]
|
||||||
}
|
// public string Username { get; set; }
|
||||||
|
// [FromForm(Name = "picture")]
|
||||||
public class AccountData
|
// public IFormFile Picture { get; set; }
|
||||||
{
|
// }
|
||||||
[FromForm(Name = "email")]
|
//
|
||||||
public string Email { get; set; }
|
//
|
||||||
[FromForm(Name = "username")]
|
// /// <summary>
|
||||||
public string Username { get; set; }
|
// /// The class responsible for login, logout, permissions and claims of a user.
|
||||||
[FromForm(Name = "picture")]
|
// /// </summary>
|
||||||
public IFormFile Picture { get; set; }
|
// [Route("api/account")]
|
||||||
}
|
// [Route("api/accounts")]
|
||||||
|
// [ApiController]
|
||||||
|
// public class AccountApi : Controller, IProfileService
|
||||||
[Route("api/[controller]")]
|
// {
|
||||||
[ApiController]
|
// private readonly UserManager<User> _userManager;
|
||||||
public class AccountController : Controller, IProfileService
|
// private readonly SignInManager<User> _signInManager;
|
||||||
{
|
// private readonly IConfiguration _configuration;
|
||||||
private readonly UserManager<User> _userManager;
|
// private readonly string _picturePath;
|
||||||
private readonly SignInManager<User> _signInManager;
|
//
|
||||||
private readonly IConfiguration _configuration;
|
// // TODO find how SignInManager & UserManager are implement and check if they can be used or not.
|
||||||
private readonly string _picturePath;
|
// public AccountApi(UserManager<User> userManager,
|
||||||
|
// SignInManager<User> signInManager,
|
||||||
public AccountController(UserManager<User> userManager,
|
// IConfiguration configuration)
|
||||||
SignInManager<User> siginInManager,
|
// {
|
||||||
IConfiguration configuration)
|
// _userManager = userManager;
|
||||||
{
|
// _signInManager = signInManager;
|
||||||
_userManager = userManager;
|
// _picturePath = configuration.GetValue<string>("profilePicturePath");
|
||||||
_signInManager = siginInManager;
|
// _configuration = configuration;
|
||||||
_picturePath = configuration.GetValue<string>("profilePicturePath");
|
// if (!Path.IsPathRooted(_picturePath))
|
||||||
_configuration = configuration;
|
// _picturePath = Path.GetFullPath(_picturePath);
|
||||||
if (!Path.IsPathRooted(_picturePath))
|
// }
|
||||||
_picturePath = Path.GetFullPath(_picturePath);
|
//
|
||||||
}
|
// [HttpPost("register")]
|
||||||
|
// public async Task<IActionResult> Register([FromBody] RegisterRequest request)
|
||||||
[HttpPost("register")]
|
// {
|
||||||
public async Task<IActionResult> Register([FromBody] RegisterRequest user)
|
// User user = request.ToUser();
|
||||||
{
|
// IdentityResult result = await _userManager.CreateAsync(user, user.Password);
|
||||||
if (!ModelState.IsValid)
|
// if (!result.Succeeded)
|
||||||
return BadRequest(user);
|
// return BadRequest(result.Errors);
|
||||||
if (user.Username.Length < 4)
|
// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
|
||||||
return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}});
|
// await _userManager.UpdateAsync(account);
|
||||||
if (!new EmailAddressAttribute().IsValid(user.Email))
|
// await _userManager.AddClaimAsync(account, new Claim(
|
||||||
return BadRequest(new[] {new {code = "email", description = "Email must be valid."}});
|
// "permissions",
|
||||||
User account = new() {UserName = user.Username, Email = user.Email};
|
// _configuration.GetValue<string>("newUserPermissions")));
|
||||||
IdentityResult result = await _userManager.CreateAsync(account, user.Password);
|
// return Ok(new {otac});
|
||||||
if (!result.Succeeded)
|
// }
|
||||||
return BadRequest(result.Errors);
|
//
|
||||||
string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
|
// [HttpPost("login")]
|
||||||
await _userManager.UpdateAsync(account);
|
// public async Task<IActionResult> Login([FromBody] LoginRequest login)
|
||||||
await _userManager.AddClaimAsync(account, new Claim(
|
// {
|
||||||
"permissions",
|
// if (!ModelState.IsValid)
|
||||||
_configuration.GetValue<string>("newUserPermissions")));
|
// return BadRequest(login);
|
||||||
return Ok(new {otac});
|
// SignInResult result = await _signInManager
|
||||||
}
|
// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false);
|
||||||
|
// if (result.Succeeded)
|
||||||
[HttpPost("login")]
|
// return Ok();
|
||||||
public async Task<IActionResult> Login([FromBody] LoginRequest login)
|
// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}});
|
||||||
{
|
// }
|
||||||
if (!ModelState.IsValid)
|
//
|
||||||
return BadRequest(login);
|
// [HttpPost("otac-login")]
|
||||||
SignInResult result = await _signInManager
|
// public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
|
||||||
.PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false);
|
// {
|
||||||
if (result.Succeeded)
|
// if (!ModelState.IsValid)
|
||||||
return Ok();
|
// return BadRequest(otac);
|
||||||
return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}});
|
// 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."}});
|
||||||
[HttpPost("otac-login")]
|
// if (user.OTACExpires <= DateTime.UtcNow)
|
||||||
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
|
// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
|
||||||
{
|
// await _signInManager.SignInAsync(user, otac.StayLoggedIn);
|
||||||
if (!ModelState.IsValid)
|
// return Ok();
|
||||||
return BadRequest(otac);
|
// }
|
||||||
User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac);
|
//
|
||||||
if (user == null)
|
// [HttpGet("logout")]
|
||||||
return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}});
|
// [Authorize]
|
||||||
if (user.OTACExpires <= DateTime.UtcNow)
|
// public async Task<IActionResult> Logout()
|
||||||
return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
|
// {
|
||||||
await _signInManager.SignInAsync(user, otac.StayLoggedIn);
|
// await _signInManager.SignOutAsync();
|
||||||
return Ok();
|
// return Ok();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
[HttpGet("logout")]
|
// public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||||
[Authorize]
|
// {
|
||||||
public async Task<IActionResult> Logout()
|
// User user = await _userManager.GetUserAsync(context.Subject);
|
||||||
{
|
// if (user != null)
|
||||||
await _signInManager.SignOutAsync();
|
// {
|
||||||
return Ok();
|
// List<Claim> claims = new()
|
||||||
}
|
// {
|
||||||
|
// new Claim("email", user.Email),
|
||||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
// new Claim("username", user.UserName),
|
||||||
{
|
// new Claim("picture", $"api/account/picture/{user.UserName}")
|
||||||
User user = await _userManager.GetUserAsync(context.Subject);
|
// };
|
||||||
if (user != null)
|
//
|
||||||
{
|
// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
|
||||||
List<Claim> claims = new()
|
// if (perms != null)
|
||||||
{
|
// claims.Add(perms);
|
||||||
new Claim("email", user.Email),
|
//
|
||||||
new Claim("username", user.UserName),
|
// context.IssuedClaims.AddRange(claims);
|
||||||
new Claim("picture", $"api/account/picture/{user.UserName}")
|
// }
|
||||||
};
|
// }
|
||||||
|
//
|
||||||
Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
|
// public async Task IsActiveAsync(IsActiveContext context)
|
||||||
if (perms != null)
|
// {
|
||||||
claims.Add(perms);
|
// User user = await _userManager.GetUserAsync(context.Subject);
|
||||||
|
// context.IsActive = user != null;
|
||||||
context.IssuedClaims.AddRange(claims);
|
// }
|
||||||
}
|
//
|
||||||
}
|
// [HttpGet("picture/{username}")]
|
||||||
|
// public async Task<IActionResult> GetPicture(string username)
|
||||||
public async Task IsActiveAsync(IsActiveContext context)
|
// {
|
||||||
{
|
// User user = await _userManager.FindByNameAsync(username);
|
||||||
User user = await _userManager.GetUserAsync(context.Subject);
|
// if (user == null)
|
||||||
context.IsActive = user != null;
|
// return BadRequest();
|
||||||
}
|
// string path = Path.Combine(_picturePath, user.Id);
|
||||||
|
// if (!System.IO.File.Exists(path))
|
||||||
[HttpGet("picture/{username}")]
|
// return NotFound();
|
||||||
public async Task<IActionResult> GetPicture(string username)
|
// return new PhysicalFileResult(path, "image/png");
|
||||||
{
|
// }
|
||||||
User user = await _userManager.FindByNameAsync(username);
|
//
|
||||||
if (user == null)
|
// [HttpPost("update")]
|
||||||
return BadRequest();
|
// [Authorize]
|
||||||
string path = Path.Combine(_picturePath, user.Id);
|
// public async Task<IActionResult> Update([FromForm] AccountData data)
|
||||||
if (!System.IO.File.Exists(path))
|
// {
|
||||||
return NotFound();
|
// User user = await _userManager.GetUserAsync(HttpContext.User);
|
||||||
return new PhysicalFileResult(path, "image/png");
|
//
|
||||||
}
|
// if (!string.IsNullOrEmpty(data.Email))
|
||||||
|
// user.Email = data.Email;
|
||||||
[HttpPost("update")]
|
// if (!string.IsNullOrEmpty(data.Username))
|
||||||
[Authorize]
|
// user.UserName = data.Username;
|
||||||
public async Task<IActionResult> Update([FromForm] AccountData data)
|
// if (data.Picture?.Length > 0)
|
||||||
{
|
// {
|
||||||
User user = await _userManager.GetUserAsync(HttpContext.User);
|
// string path = Path.Combine(_picturePath, user.Id);
|
||||||
|
// await using FileStream file = System.IO.File.Create(path);
|
||||||
if (!string.IsNullOrEmpty(data.Email))
|
// await data.Picture.CopyToAsync(file);
|
||||||
user.Email = data.Email;
|
// }
|
||||||
if (!string.IsNullOrEmpty(data.Username))
|
// await _userManager.UpdateAsync(user);
|
||||||
user.UserName = data.Username;
|
// return Ok();
|
||||||
if (data.Picture?.Length > 0)
|
// }
|
||||||
{
|
//
|
||||||
string path = Path.Combine(_picturePath, user.Id);
|
// [HttpGet("default-permissions")]
|
||||||
await using FileStream file = System.IO.File.Create(path);
|
// public ActionResult<IEnumerable<string>> GetDefaultPermissions()
|
||||||
await data.Picture.CopyToAsync(file);
|
// {
|
||||||
}
|
// return _configuration.GetValue<string>("defaultPermissions").Split(",");
|
||||||
await _userManager.UpdateAsync(user);
|
// }
|
||||||
return Ok();
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
[HttpGet("default-permissions")]
|
|
||||||
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
|
|
||||||
{
|
|
||||||
return _configuration.GetValue<string>("defaultPermissions").Split(",");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +1,23 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Kyoo.Models
|
namespace Kyoo.Models
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An interface to represent a resource that can be retrieved from the database.
|
||||||
|
/// </summary>
|
||||||
public interface IResource
|
public interface IResource
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||||
|
/// </summary>
|
||||||
public int ID { get; set; }
|
public int ID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A human-readable identifier that can be used instead of an ID.
|
||||||
|
/// A slug must be unique for a type of resource but it can be changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// There is no setter for a slug since it can be computed from other fields.
|
||||||
|
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
|
||||||
|
/// </remarks>
|
||||||
public string Slug { get; }
|
public string Slug { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResourceComparer<T> : IEqualityComparer<T> where T : IResource
|
|
||||||
{
|
|
||||||
public bool Equals(T x, T y)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(x, y))
|
|
||||||
return true;
|
|
||||||
if (ReferenceEquals(x, null))
|
|
||||||
return false;
|
|
||||||
if (ReferenceEquals(y, null))
|
|
||||||
return false;
|
|
||||||
return x.ID == y.ID || x.Slug == y.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(T obj)
|
|
||||||
{
|
|
||||||
return HashCode.Combine(obj.ID, obj.Slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
41
Kyoo.Common/Models/Resources/User.cs
Normal file
41
Kyoo.Common/Models/Resources/User.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Kyoo.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single user of the app.
|
||||||
|
/// </summary>
|
||||||
|
public class User : IResource
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int ID { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A username displayed to the user.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user email address.
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of shows the user has finished.
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<Show> Watched { get; set; }
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
}
|
@ -712,72 +712,6 @@ namespace Kyoo
|
|||||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Expression<Func<T, bool>> ResourceEquals<T>(IResource obj)
|
|
||||||
where T : IResource
|
|
||||||
{
|
|
||||||
if (obj.ID > 0)
|
|
||||||
return x => x.ID == obj.ID || x.Slug == obj.Slug;
|
|
||||||
return x => x.Slug == obj.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Func<T, bool> ResourceEqualsFunc<T>(IResource obj)
|
|
||||||
where T : IResource
|
|
||||||
{
|
|
||||||
if (obj.ID > 0)
|
|
||||||
return x => x.ID == obj.ID || x.Slug == obj.Slug;
|
|
||||||
return x => x.Slug == obj.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ResourceEquals([CanBeNull] object first, [CanBeNull] object second)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(first, second))
|
|
||||||
return true;
|
|
||||||
if (first is IResource f && second is IResource s)
|
|
||||||
return ResourceEquals(f, s);
|
|
||||||
IEnumerable eno = first as IEnumerable;
|
|
||||||
IEnumerable ens = second as IEnumerable;
|
|
||||||
if (eno == null || ens == null)
|
|
||||||
throw new ArgumentException("Arguments are not resources or lists of resources.");
|
|
||||||
Type type = GetEnumerableType(eno);
|
|
||||||
if (typeof(IResource).IsAssignableFrom(type))
|
|
||||||
return ResourceEquals(eno.Cast<IResource>(), ens.Cast<IResource>());
|
|
||||||
return RunGenericMethod<bool>(typeof(Enumerable), "SequenceEqual", type, first, second);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ResourceEquals<T>([CanBeNull] T first, [CanBeNull] T second)
|
|
||||||
where T : IResource
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(first, second))
|
|
||||||
return true;
|
|
||||||
if (first == null || second == null)
|
|
||||||
return false;
|
|
||||||
return first.ID == second.ID || first.Slug == second.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ResourceEquals<T>([CanBeNull] IEnumerable<T> first, [CanBeNull] IEnumerable<T> second)
|
|
||||||
where T : IResource
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(first, second))
|
|
||||||
return true;
|
|
||||||
if (first == null || second == null)
|
|
||||||
return false;
|
|
||||||
return first.SequenceEqual(second, new ResourceComparer<T>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool LinkEquals<T>([CanBeNull] T first, int? firstID, [CanBeNull] T second, int? secondID)
|
|
||||||
where T : IResource
|
|
||||||
{
|
|
||||||
if (ResourceEquals(first, second))
|
|
||||||
return true;
|
|
||||||
if (first == null && second != null
|
|
||||||
&& firstID == second.ID)
|
|
||||||
return true;
|
|
||||||
if (first != null && second == null
|
|
||||||
&& first.ID == secondID)
|
|
||||||
return true;
|
|
||||||
return firstID == secondID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a friendly type name (supporting generics)
|
/// Get a friendly type name (supporting generics)
|
||||||
/// For example a list of string will be displayed as List<string> and not as List`1.
|
/// For example a list of string will be displayed as List<string> and not as List`1.
|
||||||
|
@ -64,6 +64,10 @@ namespace Kyoo
|
|||||||
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
|
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<MetadataID> MetadataIds { get; set; }
|
public DbSet<MetadataID> MetadataIds { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The list of registered users.
|
||||||
|
/// </summary>
|
||||||
|
// public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All people's role. See <see cref="PeopleRole"/>.
|
/// All people's role. See <see cref="PeopleRole"/>.
|
||||||
|
@ -124,6 +124,8 @@ namespace Kyoo
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
_plugins.ConfigureAspnet(app);
|
||||||
|
|
||||||
app.UseSpa(spa =>
|
app.UseSpa(spa =>
|
||||||
{
|
{
|
||||||
@ -133,8 +135,6 @@ namespace Kyoo
|
|||||||
spa.UseAngularCliServer("start");
|
spa.UseAngularCliServer("start");
|
||||||
});
|
});
|
||||||
|
|
||||||
_plugins.ConfigureAspnet(app);
|
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
|
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"default": ["read", "play", "write", "admin"],
|
"default": ["read", "play", "write", "admin"],
|
||||||
"newUser": ["read", "play", "write", "admin"]
|
"newUser": ["read", "play", "write", "admin"]
|
||||||
}
|
},
|
||||||
|
"profilePicturePath": "users/"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +48,6 @@
|
|||||||
"transcodeTempPath": "cached/kyoo/transcode",
|
"transcodeTempPath": "cached/kyoo/transcode",
|
||||||
"peoplePath": "people",
|
"peoplePath": "people",
|
||||||
"providerPath": "providers",
|
"providerPath": "providers",
|
||||||
"profilePicturePath": "users/",
|
|
||||||
"plugins": "plugins/",
|
"plugins": "plugins/",
|
||||||
"regex": "(?:\\/(?<Collection>.*?))?\\/(?<Show>.*?)(?: \\(\\d+\\))?\\/\\k<Show>(?: \\(\\d+\\))?(?:(?: S(?<Season>\\d+)E(?<Episode>\\d+))| (?<Absolute>\\d+))?.*$",
|
"regex": "(?:\\/(?<Collection>.*?))?\\/(?<Show>.*?)(?: \\(\\d+\\))?\\/\\k<Show>(?: \\(\\d+\\))?(?:(?: S(?<Season>\\d+)E(?<Episode>\\d+))| (?<Absolute>\\d+))?.*$",
|
||||||
"subtitleRegex": "^(?<Episode>.*)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
|
"subtitleRegex": "^(?<Episode>.*)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user