diff --git a/API.Tests/Converters/CronConverterTests.cs b/API.Tests/Converters/CronConverterTests.cs index df1ca6294..34efbd59e 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/API.Tests/Converters/CronConverterTests.cs @@ -1,18 +1,10 @@ using API.Helpers.Converters; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Converters { public class CronConverterTests { - private readonly ITestOutputHelper _testOutputHelper; - - public CronConverterTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - [Theory] [InlineData("daily", "0 0 * * *")] [InlineData("disabled", "0 0 31 2 *")] diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 34b7798a3..d91e82092 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using API.Entities; +using API.Entities.Enums; using API.Parser; using Xunit; using static API.Parser.Parser; @@ -109,7 +109,10 @@ namespace API.Tests [InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")] + [InlineData("Yumekui-Merry DKThiasScanlations Chapter51v2", "51")] + [InlineData("Goblin Slayer Side Story - Year One 017.5", "17.5")] [InlineData("Beelzebub_53[KSH].zip", "53")] + [InlineData("Black Bullet - v4 c20.5 [batoto]", "20.5")] //[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")] public void ParseChaptersTest(string filename, string expected) { diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 80ee0438e..fcf4b0099 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,8 +1,4 @@ -using API.Interfaces; -using API.Services; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; +using Xunit; namespace API.Tests.Services { diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 1406ce1e7..483f5a8eb 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -63,6 +63,7 @@ namespace API.Controllers } var user = _mapper.Map(registerDto); + user.UserPreferences ??= new AppUserPreferences(); var result = await _userManager.CreateAsync(user, registerDto.Password); @@ -83,13 +84,14 @@ namespace API.Controllers lib.AppUsers ??= new List(); lib.AppUsers.Add(user); } - if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually."); + if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually."); } return new UserDto { Username = user.UserName, Token = await _tokenService.CreateToken(user), + Preferences = _mapper.Map(user.UserPreferences) }; } @@ -97,11 +99,12 @@ namespace API.Controllers public async Task> Login(LoginDto loginDto) { var user = await _userManager.Users + .Include(u => u.UserPreferences) .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); var debugUsers = await _userManager.Users.Select(x => x.NormalizedUserName).ToListAsync(); - _logger.LogInformation($"All Users: {String.Join(",", debugUsers)}"); + _logger.LogInformation($"All Users: {string.Join(",", debugUsers)}"); if (user == null) return Unauthorized("Invalid username"); @@ -112,6 +115,8 @@ namespace API.Controllers // Update LastActive on account user.LastActive = DateTime.Now; + user.UserPreferences ??= new AppUserPreferences(); + _unitOfWork.UserRepository.Update(user); await _unitOfWork.Complete(); @@ -120,7 +125,8 @@ namespace API.Controllers return new UserDto { Username = user.UserName, - Token = await _tokenService.CreateToken(user) + Token = await _tokenService.CreateToken(user), + Preferences = _mapper.Map(user.UserPreferences) }; } } diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs new file mode 100644 index 000000000..d8176279d --- /dev/null +++ b/API/Controllers/ServerController.cs @@ -0,0 +1,30 @@ +using API.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + [Authorize(Policy = "RequireAdminRole")] + public class ServerController : BaseApiController + { + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly ILogger _logger; + + public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger) + { + _applicationLifetime = applicationLifetime; + _logger = logger; + } + + [HttpPost("restart")] + public ActionResult RestartServer() + { + _logger.LogInformation($"{User.GetUsername()} is restarting server from admin dashboard."); + + _applicationLifetime.StopApplication(); + return Ok(); + } + } +} \ No newline at end of file diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index eecece06c..f7e937314 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using API.DTOs; -using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Helpers.Converters; using API.Interfaces; @@ -62,6 +63,13 @@ namespace API.Controllers setting.Value = updateSettingsDto.TaskScan; _unitOfWork.SettingsRepository.Update(setting); } + + if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + "" != setting.Value) + { + setting.Value = updateSettingsDto.Port + ""; + Environment.SetEnvironmentVariable("KAVITA_PORT", setting.Value); + _unitOfWork.SettingsRepository.Update(setting); + } } if (_unitOfWork.HasChanges() && await _unitOfWork.Complete()) diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 6a98eedd9..607f508e9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -44,5 +44,25 @@ namespace API.Controllers var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()); return Ok(libs.Any(x => x.Id == libraryId)); } + + [HttpPost("update-preferences")] + public async Task> UpdatePreferences(UserPreferencesDto preferencesDto) + { + var existingPreferences = await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()); + + existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; + existingPreferences.ScalingOption = preferencesDto.ScalingOption; + existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; + existingPreferences.HideReadOnDetails = preferencesDto.HideReadOnDetails; + + _unitOfWork.UserRepository.Update(existingPreferences); + + if (await _unitOfWork.Complete()) + { + return Ok(preferencesDto); + } + + return BadRequest("There was an issue saving preferences."); + } } } \ No newline at end of file diff --git a/API/DTOs/CreateLibraryDto.cs b/API/DTOs/CreateLibraryDto.cs index 5dcdbf4e3..f33430a6c 100644 --- a/API/DTOs/CreateLibraryDto.cs +++ b/API/DTOs/CreateLibraryDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities; +using API.Entities.Enums; namespace API.DTOs { diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index e23268269..ed741642c 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using API.Entities; +using API.Entities.Enums; namespace API.DTOs { diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 8cf706ea8..26bed91b8 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -1,4 +1,4 @@ -using API.Entities; +using API.Entities.Enums; namespace API.DTOs { diff --git a/API/DTOs/ServerSettingDTO.cs b/API/DTOs/ServerSettingDTO.cs index e16d16506..a1617ff11 100644 --- a/API/DTOs/ServerSettingDTO.cs +++ b/API/DTOs/ServerSettingDTO.cs @@ -6,5 +6,6 @@ public string TaskScan { get; set; } public string LoggingLevel { get; set; } public string TaskBackup { get; set; } + public int Port { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index eb0bf6bf2..052fa226e 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -1,8 +1,10 @@ -namespace API.DTOs + +namespace API.DTOs { public class UserDto { public string Username { get; init; } public string Token { get; init; } + public UserPreferencesDto Preferences { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs new file mode 100644 index 000000000..bec209a5b --- /dev/null +++ b/API/DTOs/UserPreferencesDto.cs @@ -0,0 +1,15 @@ +using API.Entities.Enums; + +namespace API.DTOs +{ + public class UserPreferencesDto + { + public ReadingDirection ReadingDirection { get; set; } + public ScalingOption ScalingOption { get; set; } + public PageSplitOption PageSplitOption { get; set; } + /// + /// Whether UI hides read Volumes on Details page + /// + public bool HideReadOnDetails { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 39872c05a..e9e08ec72 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -1,4 +1,5 @@  +using System; using System.Collections.Generic; namespace API.DTOs @@ -11,6 +12,9 @@ namespace API.DTOs public byte[] CoverImage { get; set; } public int Pages { get; set; } public int PagesRead { get; set; } + public DateTime LastModified { get; set; } + public DateTime Created { get; set; } + public bool IsSpecial { get; set; } public ICollection Chapters { get; set; } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 6aea9a959..24a2dae47 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -28,6 +28,7 @@ namespace API.Data public DbSet AppUserProgresses { get; set; } public DbSet AppUserRating { get; set; } public DbSet ServerSetting { get; set; } + public DbSet AppUserPreferences { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs b/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs new file mode 100644 index 000000000..1bea7a402 --- /dev/null +++ b/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs @@ -0,0 +1,718 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210205220227_UserPreferences")] + partial class UserPreferences + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HideReadOnDetails") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210205220227_UserPreferences.cs b/API/Data/Migrations/20210205220227_UserPreferences.cs new file mode 100644 index 000000000..892eb9767 --- /dev/null +++ b/API/Data/Migrations/20210205220227_UserPreferences.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class UserPreferences : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppUserPreferences", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ReadingDirection = table.Column(type: "INTEGER", nullable: false), + ScalingOption = table.Column(type: "INTEGER", nullable: false), + PageSplitOption = table.Column(type: "INTEGER", nullable: false), + HideReadOnDetails = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserPreferences", x => x.Id); + table.ForeignKey( + name: "FK_AppUserPreferences_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserPreferences_AppUserId", + table: "AppUserPreferences", + column: "AppUserId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index fcbfaa084..c5066809c 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -118,6 +118,35 @@ namespace API.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HideReadOnDetails") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => { b.Property("Id") @@ -486,6 +515,17 @@ namespace API.Data.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -644,6 +684,8 @@ namespace API.Data.Migrations b.Navigation("Ratings"); + b.Navigation("UserPreferences"); + b.Navigation("UserRoles"); }); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index c7a216215..7ba7ebeda 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Entities; +using API.Entities.Enums; using API.Services; using Microsoft.AspNetCore.Identity; @@ -30,12 +31,15 @@ namespace API.Data public static async Task SeedSettings(DataContext context) { - context.Database.EnsureCreated(); + await context.Database.EnsureCreatedAsync(); IList defaultSettings = new List() { new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory}, - new () {Key = ServerSettingKey.TaskScan, Value = "daily"} + new () {Key = ServerSettingKey.TaskScan, Value = "daily"}, + //new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, + //new () {Key = ServerSettingKey.TaskBackup, Value = "daily"}, + new () {Key = ServerSettingKey.Port, Value = "5000"}, }; foreach (var defaultSetting in defaultSettings) @@ -43,7 +47,7 @@ namespace API.Data var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key); if (existing == null) { - context.ServerSetting.Add(defaultSetting); + await context.ServerSetting.AddAsync(defaultSetting); } } diff --git a/API/Data/SettingsRepository.cs b/API/Data/SettingsRepository.cs index 21e33deb2..ecacf0f87 100644 --- a/API/Data/SettingsRepository.cs +++ b/API/Data/SettingsRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Interfaces; using AutoMapper; using Microsoft.EntityFrameworkCore; diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index a06be47ee..4f86ca5b5 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -25,6 +25,11 @@ namespace API.Data { _context.Entry(user).State = EntityState.Modified; } + + public void Update(AppUserPreferences preferences) + { + _context.Entry(preferences).State = EntityState.Modified; + } public void Delete(AppUser user) { @@ -59,6 +64,13 @@ namespace API.Data _context.AppUserRating.Add(userRating); } + public async Task GetPreferencesAsync(string username) + { + return await _context.AppUserPreferences + .Include(p => p.AppUser) + .SingleOrDefaultAsync(p => p.AppUser.UserName == username); + } + public async Task> GetMembersAsync() { return await _context.Users diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 3384df9ee..49df4d7af 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -15,6 +15,7 @@ namespace API.Entities public ICollection UserRoles { get; set; } public ICollection Progresses { get; set; } public ICollection Ratings { get; set; } + public AppUserPreferences UserPreferences { get; set; } [ConcurrencyCheck] public uint RowVersion { get; set; } diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs new file mode 100644 index 000000000..1a2e6b41b --- /dev/null +++ b/API/Entities/AppUserPreferences.cs @@ -0,0 +1,21 @@ +using API.Entities.Enums; + +namespace API.Entities +{ + public class AppUserPreferences + { + public int Id { get; set; } + public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; + public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight; + public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft; + /// + /// Whether UI hides read Volumes on Details page + /// + public bool HideReadOnDetails { get; set; } = false; + + + + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/LibraryType.cs b/API/Entities/Enums/LibraryType.cs similarity index 90% rename from API/Entities/LibraryType.cs rename to API/Entities/Enums/LibraryType.cs index 944279a25..f1a7d0fd6 100644 --- a/API/Entities/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities +namespace API.Entities.Enums { public enum LibraryType { diff --git a/API/Entities/MangaFormat.cs b/API/Entities/Enums/MangaFormat.cs similarity index 88% rename from API/Entities/MangaFormat.cs rename to API/Entities/Enums/MangaFormat.cs index 960f72145..31ebd5bb3 100644 --- a/API/Entities/MangaFormat.cs +++ b/API/Entities/Enums/MangaFormat.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities +namespace API.Entities.Enums { public enum MangaFormat { diff --git a/API/Entities/Enums/PageSplitOption.cs b/API/Entities/Enums/PageSplitOption.cs new file mode 100644 index 000000000..ae44530c7 --- /dev/null +++ b/API/Entities/Enums/PageSplitOption.cs @@ -0,0 +1,9 @@ +namespace API.Entities.Enums +{ + public enum PageSplitOption + { + SplitLeftToRight = 0, + SplitRightToLeft = 1, + NoSplit = 2 + } +} \ No newline at end of file diff --git a/API/Entities/Enums/ReadingDirection.cs b/API/Entities/Enums/ReadingDirection.cs new file mode 100644 index 000000000..e702970c9 --- /dev/null +++ b/API/Entities/Enums/ReadingDirection.cs @@ -0,0 +1,8 @@ +namespace API.Entities.Enums +{ + public enum ReadingDirection + { + LeftToRight = 0, + RightToLeft = 1 + } +} \ No newline at end of file diff --git a/API/Entities/Enums/ScalingOption.cs b/API/Entities/Enums/ScalingOption.cs new file mode 100644 index 000000000..7e144100c --- /dev/null +++ b/API/Entities/Enums/ScalingOption.cs @@ -0,0 +1,9 @@ +namespace API.Entities.Enums +{ + public enum ScalingOption + { + FitToHeight = 0, + FitToWidth = 1, + Original = 2 + } +} \ No newline at end of file diff --git a/API/Entities/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs similarity index 61% rename from API/Entities/ServerSettingKey.cs rename to API/Entities/Enums/ServerSettingKey.cs index 19b2f9f9c..214e6fc22 100644 --- a/API/Entities/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -1,10 +1,11 @@ -namespace API.Entities +namespace API.Entities.Enums { public enum ServerSettingKey { TaskScan = 0, CacheDirectory = 1, TaskBackup = 2, - LoggingLevel = 3 + LoggingLevel = 3, + Port = 4 } } \ No newline at end of file diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 3c2129c37..faf95e149 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using API.Entities.Enums; using API.Entities.Interfaces; namespace API.Entities diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index a1e4ff81d..7d001fcb0 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -1,4 +1,6 @@  +using API.Entities.Enums; + namespace API.Entities { public class MangaFile diff --git a/API/Entities/ServerSetting.cs b/API/Entities/ServerSetting.cs index 148286cdf..09501c20e 100644 --- a/API/Entities/ServerSetting.cs +++ b/API/Entities/ServerSetting.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using API.Entities.Enums; using API.Entities.Interfaces; namespace API.Entities diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 744fc08bb..4994cbb0d 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -21,6 +21,8 @@ namespace API.Helpers CreateMap(); + CreateMap(); + CreateMap() .ForMember(dest => dest.Folders, opt => diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 1795a9ba0..27d1cbbae 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using API.DTOs; using API.Entities; +using API.Entities.Enums; using AutoMapper; namespace API.Helpers.Converters @@ -26,7 +27,9 @@ namespace API.Helpers.Converters case ServerSettingKey.TaskBackup: destination.TaskBackup = row.Value; break; - + case ServerSettingKey.Port: + destination.Port = int.Parse(row.Value); + break; } } diff --git a/API/Interfaces/ISettingsRepository.cs b/API/Interfaces/ISettingsRepository.cs index fd9657678..5b0994d41 100644 --- a/API/Interfaces/ISettingsRepository.cs +++ b/API/Interfaces/ISettingsRepository.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; namespace API.Interfaces { diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 20f515350..6821da667 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -8,11 +8,13 @@ namespace API.Interfaces public interface IUserRepository { void Update(AppUser user); + void Update(AppUserPreferences preferences); public void Delete(AppUser user); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); Task> GetAdminUsersAsync(); Task GetUserRating(int seriesId, int userId); void AddRatingTracking(AppUserRating userRating); + Task GetPreferencesAsync(string username); } } \ No newline at end of file diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index dc59d3b5c..dccdcf729 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -1,9 +1,8 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; -using API.Entities; +using API.Entities.Enums; namespace API.Parser { diff --git a/API/Parser/ParserInfo.cs b/API/Parser/ParserInfo.cs index 3288063e5..ee92ddd9f 100644 --- a/API/Parser/ParserInfo.cs +++ b/API/Parser/ParserInfo.cs @@ -1,5 +1,4 @@ - -using API.Entities; +using API.Entities.Enums; namespace API.Parser { diff --git a/API/Program.cs b/API/Program.cs index 129fbcdc2..f65bba4ff 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -13,6 +13,7 @@ namespace API { public class Program { + private static readonly int HttpPort = 5000; protected Program() { } @@ -38,7 +39,7 @@ namespace API var logger = services.GetRequiredService < ILogger>(); logger.LogError(ex, "An error occurred during migration"); } - + await host.RunAsync(); } @@ -46,7 +47,40 @@ namespace API Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseKestrel((opts) => + { + opts.ListenAnyIP(HttpPort); + }); webBuilder.UseStartup(); }); + + // private static void StartNewInstance() + // { + // //_logger.LogInformation("Starting new instance"); + // + // var module = options.RestartPath; + // + // if (string.IsNullOrWhiteSpace(module)) + // { + // module = Environment.GetCommandLineArgs()[0]; + // } + // + // // string commandLineArgsString; + // // if (options.RestartArgs != null) + // // { + // // commandLineArgsString = options.RestartArgs ?? string.Empty; + // // } + // // else + // // { + // // commandLineArgsString = string.Join( + // // ' ', + // // Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); + // // } + // + // //_logger.LogInformation("Executable: {0}", module); + // //_logger.LogInformation("Arguments: {0}", commandLineArgsString); + // + // Process.Start(module, Array.Empty); + // } } } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 96853914f..06ce2df99 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -129,6 +129,7 @@ namespace API.Services try { // TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images) // or we need to move this filtering to another area (Process) + // or we can get all files and put a check in place during Process to abandon files files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions) .ToArray(); } diff --git a/API/Services/ScannerService.cs b/API/Services/ScannerService.cs index 5f84b6e3f..8121df16f 100644 --- a/API/Services/ScannerService.cs +++ b/API/Services/ScannerService.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using API.Entities; +using API.Entities.Enums; using API.Interfaces; using API.Parser; using Microsoft.Extensions.Logging; diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 0c2a143df..a5ad6f901 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using API.Entities; +using API.Entities.Enums; using API.Helpers.Converters; using API.Interfaces; using Hangfire; diff --git a/API/Startup.cs b/API/Startup.cs index 426616e51..d7a6f77f1 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -3,6 +3,7 @@ using API.Middleware; using Hangfire; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,11 @@ namespace API services.AddApplicationServices(_config); services.AddControllers(); + services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); services.AddCors(); services.AddIdentityServices(_config); services.AddSwaggerGen(c => @@ -45,11 +51,16 @@ namespace API app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1")); app.UseHangfireDashboard(); } + + app.UseForwardedHeaders(); app.UseRouting(); // Ordering is important. Cors, authentication, authorization - app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); + if (env.IsDevelopment()) + { + app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); + } app.UseAuthentication(); @@ -61,6 +72,7 @@ namespace API { ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default }); + app.UseEndpoints(endpoints => {