Adding a new User class

This commit is contained in:
Zoe Roux 2021-05-07 23:13:33 +02:00
parent d9cca97961
commit 77231f4f41
19 changed files with 563 additions and 119 deletions

View File

@ -4,6 +4,7 @@ using IdentityServer4.Extensions;
using IdentityServer4.Services; using IdentityServer4.Services;
using Kyoo.Authentication.Models; using Kyoo.Authentication.Models;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -36,7 +37,10 @@ namespace Kyoo.Authentication
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty; public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public ICollection<Type> Requires => ArraySegment<Type>.Empty; public ICollection<Type> Requires => new []
{
typeof(IUserRepository)
};
/// <summary> /// <summary>

View File

@ -10,16 +10,29 @@ namespace Kyoo.Authentication
/// </summary> /// </summary>
public static class Extensions public static class Extensions
{ {
public static ClaimsPrincipal ToPrincipal(this User user) /// <summary>
/// Get claims of an user.
/// </summary>
/// <param name="user">The user concerned</param>
/// <returns>The list of claims the user has</returns>
public static ICollection<Claim> GetClaims(this User user)
{ {
List<Claim> claims = new() return new[]
{ {
new Claim(JwtClaimTypes.Subject, user.ID.ToString()), new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
new Claim(JwtClaimTypes.Name, user.Username), new Claim(JwtClaimTypes.Name, user.Username),
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
}; };
}
ClaimsIdentity id = new (claims); /// <summary>
/// Convert a user to a ClaimsPrincipal.
/// </summary>
/// <param name="user">The user to convert</param>
/// <returns>A ClaimsPrincipal representing the user</returns>
public static ClaimsPrincipal ToPrincipal(this User user)
{
ClaimsIdentity id = new (user.GetClaims());
return new ClaimsPrincipal(id); return new ClaimsPrincipal(id);
} }
} }

View File

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model only used on account update requests.
/// </summary>
public class AccountUpdateRequest
{
/// <summary>
/// The new email address of the user
/// </summary>
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// The new username of the user.
/// </summary>
[MinLength(4)]
public string Username { get; set; }
/// <summary>
/// The picture icon.
/// </summary>
public IFormFile Picture { get; set; }
}
}

View File

@ -2,9 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityModel; using IdentityServer4.Extensions;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
using Kyoo.Authentication.Models.DTO; using Kyoo.Authentication.Models.DTO;
@ -12,24 +11,12 @@ using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions; using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
namespace Kyoo.Authentication.Views 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> /// <summary>
/// The class responsible for login, logout, permissions and claims of a user. /// The class responsible for login, logout, permissions and claims of a user.
/// </summary> /// </summary>
@ -46,6 +33,11 @@ namespace Kyoo.Authentication.Views
/// The identity server interaction service to login users. /// The identity server interaction service to login users.
/// </summary> /// </summary>
private readonly IIdentityServerInteractionService _interaction; private readonly IIdentityServerInteractionService _interaction;
/// <summary>
/// A file manager to send profile pictures
/// </summary>
private readonly IFileManager _files;
/// <summary> /// <summary>
/// Options about authentication. Those options are monitored and reloads are supported. /// Options about authentication. Those options are monitored and reloads are supported.
/// </summary> /// </summary>
@ -57,13 +49,16 @@ namespace Kyoo.Authentication.Views
/// </summary> /// </summary>
/// <param name="users">The user repository to create and manage users</param> /// <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="interaction">The identity server interaction service to login users.</param>
/// <param name="files">A file manager to send profile pictures</param>
/// <param name="options">Authentication options (this may be hot reloaded)</param> /// <param name="options">Authentication options (this may be hot reloaded)</param>
public AccountApi(IUserRepository users, public AccountApi(IUserRepository users,
IIdentityServerInteractionService interaction, IIdentityServerInteractionService interaction,
IFileManager files,
IOptionsMonitor<AuthenticationOptions> options) IOptionsMonitor<AuthenticationOptions> options)
{ {
_users = users; _users = users;
_interaction = interaction; _interaction = interaction;
_files = files;
_options = options; _options = options;
} }
@ -153,66 +148,51 @@ namespace Kyoo.Authentication.Views
// TODO check with the extension method // TODO check with the extension method
public async Task GetProfileDataAsync(ProfileDataRequestContext context) public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{ {
User user = await _userManager.GetUserAsync(context.Subject); User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
if (user != null) if (user == null)
{ return;
List<Claim> claims = new() context.IssuedClaims.AddRange(user.GetClaims());
{
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) public async Task IsActiveAsync(IsActiveContext context)
{ {
User user = await _userManager.GetUserAsync(context.Subject); User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
context.IsActive = user != null; context.IsActive = user != null;
} }
[HttpGet("picture/{username}")] [HttpGet("picture/{slug}")]
public async Task<IActionResult> GetPicture(string username) public async Task<IActionResult> GetPicture(string slug)
{ {
User user = await _userManager.FindByNameAsync(username); User user = await _users.GetOrDefault(slug);
if (user == null) if (user == null)
return BadRequest();
string path = Path.Combine(_picturePath, user.Id);
if (!System.IO.File.Exists(path))
return NotFound(); return NotFound();
return new PhysicalFileResult(path, "image/png"); string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString());
return _files.FileResult(path);
} }
[HttpPost("update")] [HttpPut]
[Authorize] [Authorize]
public async Task<IActionResult> Update([FromForm] AccountData data) public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
{ {
User user = await _userManager.GetUserAsync(HttpContext.User); User user = await _users.Get(int.Parse(HttpContext.User.GetSubjectId()));
if (!string.IsNullOrEmpty(data.Email)) if (!string.IsNullOrEmpty(data.Email))
user.Email = data.Email; user.Email = data.Email;
if (!string.IsNullOrEmpty(data.Username)) if (!string.IsNullOrEmpty(data.Username))
user.UserName = data.Username; user.Username = data.Username;
if (data.Picture?.Length > 0) if (data.Picture?.Length > 0)
{ {
string path = Path.Combine(_picturePath, user.Id); string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString());
await using FileStream file = System.IO.File.Create(path); await using Stream file = _files.NewFile(path);
await data.Picture.CopyToAsync(file); await data.Picture.CopyToAsync(file);
} }
await _userManager.UpdateAsync(user); return await _users.Edit(user, false);
return Ok();
} }
[HttpGet("default-permissions")] [HttpGet("permissions")]
public ActionResult<IEnumerable<string>> GetDefaultPermissions() public ActionResult<IEnumerable<string>> GetDefaultPermissions()
{ {
return _configuration.GetValue<string>("defaultPermissions").Split(","); return _options.CurrentValue.Permissions.Default;
} }
} }
} }

View File

@ -7,21 +7,87 @@ using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
/// </summary>
public interface IFileManager public interface IFileManager
{ {
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false);
public StreamReader GetReader([NotNull] string path);
public Task<ICollection<string>> ListFiles([NotNull] string path);
public Task<bool> Exists([NotNull] string path);
// TODO find a way to handle Transmux/Transcode with this system. // TODO find a way to handle Transmux/Transcode with this system.
/// <summary>
/// Used for http queries returning a file. This should be used to return local files
/// or proxy them from a distant server
/// </summary>
/// <remarks>
/// If no file exists at the given path, you should return a NotFoundResult or handle it gracefully.
/// </remarks>
/// <param name="path">The path of the file.</param>
/// <param name="rangeSupport">
/// Should the file be downloaded at once or is the client allowed to request only part of the file
/// </param>
/// <param name="type">
/// You can manually specify the content type of your file.
/// For example you can force a file to be returned as plain text using <c>text/plain</c>.
/// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
/// </param>
/// <returns>An <see cref="IActionResult"/> representing the file returned.</returns>
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
/// <summary>
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
/// To return files from an http endpoint, use <see cref="FileResult"/>.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns>
public Stream GetReader([NotNull] string path);
/// <summary>
/// Create a new file at <paramref name="path"></paramref>.
/// </summary>
/// <param name="path">The path of the new file.</param>
/// <returns>A writer to write to the new file.</returns>
public Stream NewFile([NotNull] string path);
/// <summary>
/// List files in a directory.
/// </summary>
/// <param name="path">The path of the directory</param>
/// <returns>A list of files's path.</returns>
public Task<ICollection<string>> ListFiles([NotNull] string path);
/// <summary>
/// Check if a file exists at the given path.
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path exists, false otherwise</returns>
public Task<bool> Exists([NotNull] string path);
/// <summary>
/// Get the extra directory of a show.
/// This method is in this system to allow a filesystem to use a different metadata policy for one.
/// It can be useful if the filesystem is readonly.
/// </summary>
/// <param name="show">The show to proceed</param>
/// <returns>The extra directory of the show</returns>
public string GetExtraDirectory(Show show); public string GetExtraDirectory(Show show);
/// <summary>
/// Get the extra directory of a season.
/// This method is in this system to allow a filesystem to use a different metadata policy for one.
/// It can be useful if the filesystem is readonly.
/// </summary>
/// <param name="season">The season to proceed</param>
/// <returns>The extra directory of the season</returns>
public string GetExtraDirectory(Season season); public string GetExtraDirectory(Season season);
/// <summary>
/// Get the extra directory of an episode.
/// This method is in this system to allow a filesystem to use a different metadata policy for one.
/// It can be useful if the filesystem is readonly.
/// </summary>
/// <param name="episode">The episode to proceed</param>
/// <returns>The extra directory of the episode</returns>
public string GetExtraDirectory(Episode episode); public string GetExtraDirectory(Episode episode);
} }
} }

View File

@ -53,11 +53,17 @@ namespace Kyoo.Models
/// Links between Users and Shows. /// Links between Users and Shows.
/// </summary> /// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; } public ICollection<Link<User, Show>> ShowLinks { get; set; }
/// <summary>
/// Links between Users and WatchedEpisodes.
/// </summary>
public ICollection<Link<User, WatchedEpisode>> EpisodeLinks { get; set; }
#endif #endif
} }
/// <summary>
/// Metadata of episode currently watching by an user
/// </summary>
public class WatchedEpisode : Link<User, Episode>
{
/// <summary>
/// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
/// </summary>
public int WatchedPercentage { get; set; }
}
} }

View File

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

View File

@ -74,6 +74,11 @@ namespace Kyoo
/// </summary> /// </summary>
public DbSet<PeopleRole> PeopleRoles { get; set; } public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
/// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
/// <summary> /// <summary>
/// Get a generic link between two resource types. /// Get a generic link between two resource types.
/// </summary> /// </summary>
@ -188,6 +193,17 @@ namespace Kyoo
.WithMany(x => x.ShowLinks), .WithMany(x => x.ShowLinks),
y => y.HasKey(Link<Show, Genre>.PrimaryKey)); y => y.HasKey(Link<Show, Genre>.PrimaryKey));
modelBuilder.Entity<User>()
.HasMany(x => x.Watched)
.WithMany("users")
.UsingEntity<Link<User, Show>>(
y => y
.HasOne(x => x.Second)
.WithMany(),
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<User, Show>.PrimaryKey));
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Show) .HasOne(x => x.Show)
@ -210,6 +226,9 @@ namespace Kyoo
.WithMany(x => x.MetadataLinks) .WithMany(x => x.MetadataLinks)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WatchedEpisode>()
.HasKey(x => new {First = x.FirstID, Second = x.SecondID});
modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
@ -217,6 +236,7 @@ namespace Kyoo
modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Collection>() modelBuilder.Entity<Collection>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
@ -248,6 +268,9 @@ namespace Kyoo
modelBuilder.Entity<Track>() modelBuilder.Entity<Track>()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique(); .IsUnique();
modelBuilder.Entity<User>()
.HasIndex(x => x.Slug)
.IsUnique();
} }
/// <summary> /// <summary>

View File

@ -2,17 +2,20 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
</PropertyGroup>
<PropertyGroup>
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath> <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile> <GenerateDependencyFile>false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles> <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -11,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations namespace Kyoo.Postgresql.Migrations
{ {
[DbContext(typeof(PostgresContext))] [DbContext(typeof(PostgresContext))]
[Migration("20210505182627_Initial")] [Migration("20210507203809_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -225,6 +226,21 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("Link<Show, Genre>"); b.ToTable("Link<Show, Genre>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
@ -509,6 +525,58 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("ExtraData")
.HasColumnType("jsonb");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string[]>("Permissions")
.HasColumnType("text[]");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.Property<int>("WatchedPercentage")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
{ {
b.HasOne("Kyoo.Models.Season", "Season") b.HasOne("Kyoo.Models.Season", "Season")
@ -621,6 +689,25 @@ namespace Kyoo.Postgresql.Migrations
b.Navigation("Second"); b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{ {
b.HasOne("Kyoo.Models.Episode", "Episode") b.HasOne("Kyoo.Models.Episode", "Episode")
@ -710,6 +797,25 @@ namespace Kyoo.Postgresql.Migrations
b.Navigation("Episode"); b.Navigation("Episode");
}); });
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b => modelBuilder.Entity("Kyoo.Models.Collection", b =>
{ {
b.Navigation("LibraryLinks"); b.Navigation("LibraryLinks");
@ -780,6 +886,13 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Navigation("Shows"); b.Navigation("Shows");
}); });
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@ -104,6 +105,24 @@ namespace Kyoo.Postgresql.Migrations
table.PrimaryKey("PK_Studios", x => x.ID); table.PrimaryKey("PK_Studios", x => x.ID);
}); });
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(type: "text", nullable: false),
Username = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
Password = table.Column<string>(type: "text", nullable: true),
Permissions = table.Column<string[]>(type: "text[]", nullable: true),
ExtraData = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.ID);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Library, Collection>", name: "Link<Library, Collection>",
columns: table => new columns: table => new
@ -256,6 +275,30 @@ namespace Kyoo.Postgresql.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "Link<User, Show>",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<User, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<User, Show>_Shows_SecondID",
column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<User, Show>_Users_FirstID",
column: x => x.FirstID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PeopleRoles", name: "PeopleRoles",
columns: table => new columns: table => new
@ -420,6 +463,31 @@ namespace Kyoo.Postgresql.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "WatchedEpisodes",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false),
WatchedPercentage = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_WatchedEpisodes_Episodes_SecondID",
column: x => x.SecondID,
principalTable: "Episodes",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WatchedEpisodes_Users_FirstID",
column: x => x.FirstID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Collections_Slug", name: "IX_Collections_Slug",
table: "Collections", table: "Collections",
@ -474,6 +542,11 @@ namespace Kyoo.Postgresql.Migrations
table: "Link<Show, Genre>", table: "Link<Show, Genre>",
column: "SecondID"); column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_Link<User, Show>_SecondID",
table: "Link<User, Show>",
column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_EpisodeID", name: "IX_MetadataIds_EpisodeID",
table: "MetadataIds", table: "MetadataIds",
@ -549,6 +622,17 @@ namespace Kyoo.Postgresql.Migrations
table: "Tracks", table: "Tracks",
columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Slug",
table: "Users",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WatchedEpisodes_SecondID",
table: "WatchedEpisodes",
column: "SecondID");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
@ -568,6 +652,9 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Show, Genre>"); name: "Link<Show, Genre>");
migrationBuilder.DropTable(
name: "Link<User, Show>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataIds"); name: "MetadataIds");
@ -577,6 +664,9 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Tracks"); name: "Tracks");
migrationBuilder.DropTable(
name: "WatchedEpisodes");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Collections"); name: "Collections");
@ -595,6 +685,9 @@ namespace Kyoo.Postgresql.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Episodes"); name: "Episodes");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Seasons"); name: "Seasons");

View File

@ -1,5 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -223,6 +224,21 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("Link<Show, Genre>"); b.ToTable("Link<Show, Genre>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
@ -507,6 +523,58 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("ExtraData")
.HasColumnType("jsonb");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string[]>("Permissions")
.HasColumnType("text[]");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.Property<int>("WatchedPercentage")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
{ {
b.HasOne("Kyoo.Models.Season", "Season") b.HasOne("Kyoo.Models.Season", "Season")
@ -619,6 +687,25 @@ namespace Kyoo.Postgresql.Migrations
b.Navigation("Second"); b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{ {
b.HasOne("Kyoo.Models.Episode", "Episode") b.HasOne("Kyoo.Models.Episode", "Episode")
@ -708,6 +795,25 @@ namespace Kyoo.Postgresql.Migrations
b.Navigation("Episode"); b.Navigation("Episode");
}); });
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b => modelBuilder.Entity("Kyoo.Models.Collection", b =>
{ {
b.Navigation("LibraryLinks"); b.Navigation("LibraryLinks");
@ -778,6 +884,13 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Navigation("Shows"); b.Navigation("Shows");
}); });
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -90,6 +90,10 @@ namespace Kyoo.Postgresql
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>(); modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<User>()
.Property(x => x.ExtraData)
.HasColumnType("jsonb");
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }

@ -1 +1 @@
Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3 Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99

View File

@ -8,10 +8,22 @@ using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A <see cref="IFileManager"/> for the local filesystem (using System.IO).
/// </summary>
public class FileManager : IFileManager public class FileManager : IFileManager
{ {
/// <summary>
/// An extension provider to get content types from files extensions.
/// </summary>
private FileExtensionContentTypeProvider _provider; private FileExtensionContentTypeProvider _provider;
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private string _GetContentType(string path) private string _GetContentType(string path)
{ {
if (_provider == null) if (_provider == null)
@ -28,26 +40,36 @@ namespace Kyoo.Controllers
throw new NotImplementedException($"Can't get the content type of the file at: {path}"); throw new NotImplementedException($"Can't get the content type of the file at: {path}");
} }
// TODO add a way to force content type /// <inheritdoc />
public IActionResult FileResult(string path, bool range) public IActionResult FileResult(string path, bool range = false, string type = null)
{ {
if (path == null) if (path == null)
return new NotFoundResult(); return new NotFoundResult();
if (!File.Exists(path)) if (!File.Exists(path))
return new NotFoundResult(); return new NotFoundResult();
return new PhysicalFileResult(Path.GetFullPath(path), _GetContentType(path)) return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
{ {
EnableRangeProcessing = range EnableRangeProcessing = range
}; };
} }
public StreamReader GetReader(string path) /// <inheritdoc />
public Stream GetReader(string path)
{ {
if (path == null) if (path == null)
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
return new StreamReader(path); return File.OpenRead(path);
} }
/// <inheritdoc />
public Stream NewFile(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return File.Create(path);
}
/// <inheritdoc />
public Task<ICollection<string>> ListFiles(string path) public Task<ICollection<string>> ListFiles(string path)
{ {
if (path == null) if (path == null)
@ -57,11 +79,13 @@ namespace Kyoo.Controllers
: Array.Empty<string>()); : Array.Empty<string>());
} }
/// <inheritdoc />
public Task<bool> Exists(string path) public Task<bool> Exists(string path)
{ {
return Task.FromResult(File.Exists(path)); return Task.FromResult(File.Exists(path));
} }
/// <inheritdoc />
public string GetExtraDirectory(Show show) public string GetExtraDirectory(Show show)
{ {
string path = Path.Combine(show.Path, "Extra"); string path = Path.Combine(show.Path, "Extra");
@ -69,6 +93,7 @@ namespace Kyoo.Controllers
return path; return path;
} }
/// <inheritdoc />
public string GetExtraDirectory(Season season) public string GetExtraDirectory(Season season)
{ {
if (season.Show == null) if (season.Show == null)
@ -79,6 +104,7 @@ namespace Kyoo.Controllers
return path; return path;
} }
/// <inheritdoc />
public string GetExtraDirectory(Episode episode) public string GetExtraDirectory(Episode episode)
{ {
string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra"); string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra");

View File

@ -14,7 +14,7 @@ namespace Kyoo.Controllers
public class ShowRepository : LocalRepository<Show>, IShowRepository public class ShowRepository : LocalRepository<Show>, IShowRepository
{ {
/// <summary> /// <summary>
/// The databse handle /// The database handle
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary> /// <summary>

View File

@ -44,7 +44,8 @@ namespace Kyoo
(typeof(IPeopleRepository), typeof(DatabaseContext)), (typeof(IPeopleRepository), typeof(DatabaseContext)),
(typeof(IStudioRepository), typeof(DatabaseContext)), (typeof(IStudioRepository), typeof(DatabaseContext)),
(typeof(IGenreRepository), typeof(DatabaseContext)), (typeof(IGenreRepository), typeof(DatabaseContext)),
(typeof(IProviderRepository), typeof(DatabaseContext)) (typeof(IProviderRepository), typeof(DatabaseContext)),
(typeof(IUserRepository), typeof(DatabaseContext))
}; };
/// <inheritdoc /> /// <inheritdoc />
@ -88,6 +89,7 @@ namespace Kyoo
services.AddRepository<IStudioRepository, StudioRepository>(); services.AddRepository<IStudioRepository, StudioRepository>();
services.AddRepository<IGenreRepository, GenreRepository>(); services.AddRepository<IGenreRepository, GenreRepository>();
services.AddRepository<IProviderRepository, ProviderRepository>(); services.AddRepository<IProviderRepository, ProviderRepository>();
services.AddRepository<IUserRepository, UserRepository>();
} }
services.AddTask<Crawler>(); services.AddTask<Crawler>();

View File

@ -126,14 +126,14 @@ namespace Kyoo
app.UseResponseCompression(); app.UseResponseCompression();
_plugins.ConfigureAspnet(app); _plugins.ConfigureAspnet(app);
//
app.UseSpa(spa => // app.UseSpa(spa =>
{ // {
spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp"); // spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
//
if (env.IsDevelopment()) // if (env.IsDevelopment())
spa.UseAngularCliServer("start"); // spa.UseAngularCliServer("start");
}); // });
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {

View File

@ -71,7 +71,7 @@ namespace Kyoo.Api
await writer.WriteLineAsync(""); await writer.WriteLineAsync("");
await writer.WriteLineAsync(""); await writer.WriteLineAsync("");
using StreamReader reader = _files.GetReader(_path); using StreamReader reader = new(_files.GetReader(_path));
string line; string line;
while ((line = await reader.ReadLineAsync()) != null) while ((line = await reader.ReadLineAsync()) != null)
{ {