mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Starting to rework authentication
This commit is contained in:
parent
da699c096d
commit
0f2bea9bc4
@ -64,7 +64,7 @@ namespace Kyoo.Authentication
|
||||
/// <inheritdoc />
|
||||
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 =>
|
||||
// {
|
||||
@ -86,9 +86,9 @@ namespace Kyoo.Authentication
|
||||
services.AddIdentityServer(options =>
|
||||
{
|
||||
options.IssuerUri = publicUrl;
|
||||
options.UserInteraction.LoginUrl = publicUrl + "login";
|
||||
options.UserInteraction.ErrorUrl = publicUrl + "error";
|
||||
options.UserInteraction.LogoutUrl = publicUrl + "logout";
|
||||
options.UserInteraction.LoginUrl = $"{publicUrl}/login";
|
||||
options.UserInteraction.ErrorUrl = $"{publicUrl}/error";
|
||||
options.UserInteraction.LogoutUrl = $"{publicUrl}/logout";
|
||||
})
|
||||
// .AddAspNetIdentity<User>()
|
||||
// .AddConfigurationStore(options =>
|
||||
@ -105,11 +105,13 @@ namespace Kyoo.Authentication
|
||||
// options.EnableTokenCleanup = true;
|
||||
// })
|
||||
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
|
||||
.AddInMemoryApiScopes(IdentityContext.GetScopes())
|
||||
.AddInMemoryApiResources(IdentityContext.GetApis())
|
||||
.AddInMemoryClients(IdentityContext.GetClients())
|
||||
// .AddProfileService<AccountController>()
|
||||
.AddSigninKeys(certificateOptions);
|
||||
.AddDeveloperSigningCredential();
|
||||
// .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 =>
|
||||
{
|
||||
|
@ -17,9 +17,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.AccessTokenValidation" Version="3.0.1" />
|
||||
<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 IdentityModel;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
public class User : IdentityUser
|
||||
{
|
||||
public string OTAC { get; set; }
|
||||
public DateTime? OTACExpires { get; set; }
|
||||
|
||||
public string GenerateOTAC(TimeSpan validFor)
|
||||
{
|
||||
string otac = CryptoRandom.CreateUniqueId();
|
||||
string hashed = otac; // TODO should add a good hashing here.
|
||||
|
||||
OTAC = hashed;
|
||||
OTACExpires = DateTime.UtcNow.Add(validFor);
|
||||
return otac;
|
||||
}
|
||||
}
|
||||
}
|
||||
// using System;
|
||||
// using IdentityModel;
|
||||
// using Microsoft.AspNetCore.Identity;
|
||||
//
|
||||
// namespace Kyoo.Models
|
||||
// {
|
||||
// public class User : IdentityUser
|
||||
// {
|
||||
// public string OTAC { get; set; }
|
||||
// public DateTime? OTACExpires { get; set; }
|
||||
//
|
||||
// public string GenerateOTAC(TimeSpan validFor)
|
||||
// {
|
||||
// string otac = CryptoRandom.CreateUniqueId();
|
||||
// string hashed = otac; // TODO should add a good hashing here.
|
||||
//
|
||||
// OTAC = hashed;
|
||||
// OTACExpires = DateTime.UtcNow.Add(validFor);
|
||||
// return otac;
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -1,191 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Kyoo.Api
|
||||
{
|
||||
public class RegisterRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool StayLoggedIn { get; set; }
|
||||
}
|
||||
|
||||
public class OtacRequest
|
||||
{
|
||||
public string Otac { get; set; }
|
||||
public bool StayLoggedIn { get; set; }
|
||||
}
|
||||
|
||||
public class AccountData
|
||||
{
|
||||
[FromForm(Name = "email")]
|
||||
public string Email { get; set; }
|
||||
[FromForm(Name = "username")]
|
||||
public string Username { get; set; }
|
||||
[FromForm(Name = "picture")]
|
||||
public IFormFile Picture { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AccountController : Controller, IProfileService
|
||||
{
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly SignInManager<User> _signInManager;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _picturePath;
|
||||
|
||||
public AccountController(UserManager<User> userManager,
|
||||
SignInManager<User> siginInManager,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = siginInManager;
|
||||
_picturePath = configuration.GetValue<string>("profilePicturePath");
|
||||
_configuration = configuration;
|
||||
if (!Path.IsPathRooted(_picturePath))
|
||||
_picturePath = Path.GetFullPath(_picturePath);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterRequest user)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(user);
|
||||
if (user.Username.Length < 4)
|
||||
return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}});
|
||||
if (!new EmailAddressAttribute().IsValid(user.Email))
|
||||
return BadRequest(new[] {new {code = "email", description = "Email must be valid."}});
|
||||
User account = new() {UserName = user.Username, Email = user.Email};
|
||||
IdentityResult result = await _userManager.CreateAsync(account, user.Password);
|
||||
if (!result.Succeeded)
|
||||
return BadRequest(result.Errors);
|
||||
string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
|
||||
await _userManager.UpdateAsync(account);
|
||||
await _userManager.AddClaimAsync(account, new Claim(
|
||||
"permissions",
|
||||
_configuration.GetValue<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.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(",");
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -1,30 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface to represent a resource that can be retrieved from the database.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <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 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);
|
||||
}
|
||||
|
||||
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>
|
||||
/// Get a friendly type name (supporting generics)
|
||||
/// 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"/>.
|
||||
/// </summary>
|
||||
public DbSet<MetadataID> MetadataIds { get; set; }
|
||||
/// <summary>
|
||||
/// The list of registered users.
|
||||
/// </summary>
|
||||
// public DbSet<User> Users { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All people's role. See <see cref="PeopleRole"/>.
|
||||
|
@ -124,6 +124,8 @@ namespace Kyoo
|
||||
return next();
|
||||
});
|
||||
app.UseResponseCompression();
|
||||
|
||||
_plugins.ConfigureAspnet(app);
|
||||
|
||||
app.UseSpa(spa =>
|
||||
{
|
||||
@ -133,8 +135,6 @@ namespace Kyoo
|
||||
spa.UseAngularCliServer("start");
|
||||
});
|
||||
|
||||
_plugins.ConfigureAspnet(app);
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
|
||||
|
@ -33,7 +33,8 @@
|
||||
"permissions": {
|
||||
"default": ["read", "play", "write", "admin"],
|
||||
"newUser": ["read", "play", "write", "admin"]
|
||||
}
|
||||
},
|
||||
"profilePicturePath": "users/"
|
||||
},
|
||||
|
||||
|
||||
@ -47,7 +48,6 @@
|
||||
"transcodeTempPath": "cached/kyoo/transcode",
|
||||
"peoplePath": "people",
|
||||
"providerPath": "providers",
|
||||
"profilePicturePath": "users/",
|
||||
"plugins": "plugins/",
|
||||
"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\\.)?.*$"
|
||||
|
Loading…
x
Reference in New Issue
Block a user