diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 10c7d3583..546837fd1 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -178,6 +178,8 @@ namespace API.Tests.Parser [InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")] [InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")] [InlineData("Highschool of the Dead - Full Color Edition v02 [Uasaha] (Yen Press)", "Highschool of the Dead - Full Color Edition")] + [InlineData("諌山創] 進撃の巨人 第23巻", "諌山創] 進撃の巨人")] + [InlineData("(一般コミック) [奥浩哉] いぬやしき 第09巻", "いぬやしき")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index c2a2a3e8f..e44ce1781 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -70,13 +70,21 @@ namespace API.Controllers /// /// /// + [AllowAnonymous] [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { + // TODO: Log this request to Audit Table _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); - var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName); - if (resetPasswordDto.UserName != User.GetUsername() && !(User.IsInRole(PolicyConstants.AdminRole) || User.IsInRole(PolicyConstants.ChangePasswordRole))) + var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName); + if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system + + + if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || User.IsInRole(PolicyConstants.AdminRole))) + return Unauthorized("You are not permitted to this operation."); + + if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole)) return Unauthorized("You are not permitted to this operation."); var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password); @@ -94,6 +102,7 @@ namespace API.Controllers /// /// /// + [AllowAnonymous] [HttpPost("register")] public async Task> RegisterFirstUser(RegisterDto registerDto) { @@ -158,6 +167,7 @@ namespace API.Controllers /// /// /// + [AllowAnonymous] [HttpPost("login")] public async Task> Login(LoginDto loginDto) { @@ -176,13 +186,13 @@ namespace API.Controllers "You are missing an email on your account. Please wait while we migrate your account."); } - if (!validPassword) - { - return Unauthorized("Your credentials are not correct"); - } - var result = await _signInManager - .CheckPasswordSignInAsync(user, loginDto.Password, false); + .CheckPasswordSignInAsync(user, loginDto.Password, true); + + if (result.IsLockedOut) + { + return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes."); + } if (!result.Succeeded) { @@ -215,6 +225,7 @@ namespace API.Controllers /// /// /// + [AllowAnonymous] [HttpPost("refresh-token")] public async Task> RefreshToken([FromBody] TokenRequestDto tokenRequestDto) { @@ -486,6 +497,7 @@ namespace API.Controllers return BadRequest("There was an error setting up your account. Please check the logs"); } + [AllowAnonymous] [HttpPost("confirm-email")] public async Task> ConfirmEmail(ConfirmEmailDto dto) { diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 2c945b5fe..045cc63dc 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using API.Entities; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -18,6 +19,7 @@ namespace API.Controllers /// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup. /// /// + [AllowAnonymous] [HttpGet("exists")] public async Task> AdminExists() { diff --git a/API/Controllers/BaseApiController.cs b/API/Controllers/BaseApiController.cs index bb3886ab8..dfedd7a0a 100644 --- a/API/Controllers/BaseApiController.cs +++ b/API/Controllers/BaseApiController.cs @@ -1,10 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; namespace API.Controllers { [ApiController] [Route("api/[controller]")] + [Authorize] public class BaseApiController : ControllerBase { } -} \ No newline at end of file +} diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index ae8bad21f..a765269b8 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -1,24 +1,26 @@ using System.IO; using API.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers +namespace API.Controllers; + +[AllowAnonymous] +public class FallbackController : Controller { - public class FallbackController : Controller + // ReSharper disable once S4487 + // ReSharper disable once NotAccessedField.Local + private readonly ITaskScheduler _taskScheduler; + + public FallbackController(ITaskScheduler taskScheduler) { - // ReSharper disable once S4487 - // ReSharper disable once NotAccessedField.Local - private readonly ITaskScheduler _taskScheduler; + // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses. + _taskScheduler = taskScheduler; + } - public FallbackController(ITaskScheduler taskScheduler) - { - // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses. - _taskScheduler = taskScheduler; - } - - public ActionResult Index() - { - return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); - } + public ActionResult Index() + { + return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); } } + diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 17ce2c215..8ca49d238 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -138,6 +138,8 @@ namespace API.Controllers [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)] public ActionResult GetCoverUploadImage(string filename) { + if (filename.Contains("..")) return BadRequest("Invalid Filename"); + var path = Path.Join(_directoryService.TempDirectory, filename); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist"); var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", ""); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 168ce974c..255c38f19 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -17,10 +17,12 @@ using API.Extensions; using API.Helpers; using API.Services; using Kavita.Common; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +[AllowAnonymous] public class OpdsController : BaseApiController { private readonly IUnitOfWork _unitOfWork; diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index e9ac156a2..50290af7f 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -53,6 +53,11 @@ namespace API.Controllers var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest("There was an issue finding pdf file for reading"); + // Validate the user has access to the PDF + var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id, + await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername())); + if (series == null) return BadRequest("Invalid Access"); + try { var path = _cacheService.GetCachedFile(chapter); diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 1c326d5cc..ccd27a783 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -3,15 +3,18 @@ using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Data; +using API.Data.Repositories; using API.DTOs.ReadingLists; using API.Entities; using API.Extensions; using API.Helpers; using API.SignalR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers { + [Authorize] public class ReadingListController : BaseApiController { private readonly IUnitOfWork _unitOfWork; @@ -75,6 +78,18 @@ namespace API.Controllers return Ok(items); } + private async Task UserHasReadingListAccess(int readingListId) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), + AppUserIncludes.ReadingLists); + if (user.ReadingLists.SingleOrDefault(rl => rl.Id == readingListId) == null && !await _unitOfWork.UserRepository.IsUserAdminAsync(user)) + { + return null; + } + + return user; + } + /// /// Updates an items position /// @@ -84,6 +99,11 @@ namespace API.Controllers public async Task UpdateListItemPosition(UpdateReadingListPosition dto) { // Make sure UI buffers events + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); var item = items.Find(r => r.Id == dto.ReadingListItemId); items.Remove(item); @@ -110,10 +130,15 @@ namespace API.Controllers [HttpPost("delete-item")] public async Task DeleteListItem(UpdateReadingListPosition dto) { + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } + var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList(); - var index = 0; foreach (var readingListItem in readingList.Items) { @@ -139,9 +164,14 @@ namespace API.Controllers [HttpPost("remove-read")] public async Task DeleteReadFromList([FromQuery] int readingListId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId); - items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()); + var user = await UserHasReadingListAccess(readingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } + + var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id); + items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(user.Id, items.ToList()); // Collect all Ids to remove var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id); @@ -174,15 +204,13 @@ namespace API.Controllers [HttpDelete] public async Task DeleteList([FromQuery] int readingListId) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId); - if (readingList == null && !isAdmin) + var user = await UserHasReadingListAccess(readingListId); + if (user == null) { - return BadRequest("User is not associated with this reading list"); + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); } - readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); user.ReadingLists.Remove(readingList); @@ -211,13 +239,14 @@ namespace API.Controllers return BadRequest("A list of this name already exists"); } - user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false)); + var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false); + user.ReadingLists.Add(readingList); if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list"); await _unitOfWork.CommitAsync(); - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title)); + return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); } /// @@ -231,7 +260,11 @@ namespace API.Controllers var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); if (readingList == null) return BadRequest("List does not exist"); - + var user = await UserHasReadingListAccess(readingList.Id); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } if (!string.IsNullOrEmpty(dto.Title)) { @@ -275,7 +308,12 @@ namespace API.Controllers [HttpPost("update-by-series")] public async Task UpdateListBySeries(UpdateReadingListBySeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } + var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); var chapterIdsForSeries = @@ -312,7 +350,11 @@ namespace API.Controllers [HttpPost("update-by-multiple")] public async Task UpdateListByMultiple(UpdateReadingListByMultipleDto dto) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); @@ -352,7 +394,11 @@ namespace API.Controllers [HttpPost("update-by-multiple-series")] public async Task UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); @@ -386,9 +432,14 @@ namespace API.Controllers [HttpPost("update-by-volume")] public async Task UpdateListByVolume(UpdateReadingListByVolumeDto dto) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); + var chapterIdsForVolume = (await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); @@ -417,7 +468,11 @@ namespace API.Controllers [HttpPost("update-by-chapter")] public async Task UpdateListByChapter(UpdateReadingListByChapterDto dto) { - var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername()); + var user = await UserHasReadingListAccess(dto.ReadingListId); + if (user == null) + { + return BadRequest("You do not have permissions on this reading list or the list doesn't exist"); + } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest("Reading List does not exist"); diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index bf68e8641..69793df46 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -24,6 +24,7 @@ public class ThemeController : BaseApiController _taskScheduler = taskScheduler; } + [AllowAnonymous] [HttpGet] public async Task>> GetThemes() { diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index ca84acc8b..c7def1408 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -59,6 +59,8 @@ namespace API.Controllers if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"Could not download file"); + if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image"); + return $"coverupload_{dateString}.{format}"; } catch (FlurlHttpException ex) diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 3f909c406..20ec4a0c4 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -80,7 +80,7 @@ public class WantToReadController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.WantToRead); - user.WantToRead = user.WantToRead.Where(s => @dto.SeriesIds.Contains(s.Id)).ToList(); + user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList(); if (!_unitOfWork.HasChanges()) return Ok(); if (await _unitOfWork.CommitAsync()) return Ok(); diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs b/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs new file mode 100644 index 000000000..7ca5b6beb --- /dev/null +++ b/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs @@ -0,0 +1,1596 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20220802222910_BookmarkHasDate")] + partial class BookmarkHasDate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.7"); + + 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", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .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", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + 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.HasIndex("SeriesId"); + + 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", (string)null); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + 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.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Genre"); + }); + + 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("LastScanned") + .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("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + 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.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .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("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + 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", (string)null); + }); + + 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", (string)null); + }); + + 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", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .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.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .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.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + 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("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + 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.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.cs b/API/Data/Migrations/20220802222910_BookmarkHasDate.cs new file mode 100644 index 000000000..eee40b647 --- /dev/null +++ b/API/Data/Migrations/20220802222910_BookmarkHasDate.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class BookmarkHasDate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "AppUserBookmark", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "AppUserBookmark", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Created", + table: "AppUserBookmark"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "AppUserBookmark"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 64da8ea4f..a1fe108d7 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -137,9 +137,15 @@ namespace API.Data.Migrations b.Property("ChapterId") .HasColumnType("INTEGER"); + b.Property("Created") + .HasColumnType("TEXT"); + b.Property("FileName") .HasColumnType("TEXT"); + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("Page") .HasColumnType("INTEGER"); diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index c884973a7..6fde82929 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -17,7 +17,7 @@ public interface IReadingListRepository Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId); Task GetReadingListDtoByIdAsync(int readingListId, int userId); Task> AddReadingProgressModifiers(int userId, IList items); - Task GetReadingListDtoByTitleAsync(string title); + Task GetReadingListDtoByTitleAsync(int userId, string title); Task> GetReadingListItemsByIdAsync(int readingListId); Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, @@ -215,10 +215,10 @@ public class ReadingListRepository : IReadingListRepository return items; } - public async Task GetReadingListDtoByTitleAsync(string title) + public async Task GetReadingListDtoByTitleAsync(int userId, string title) { return await _context.ReadingList - .Where(r => r.Title.Equals(title)) + .Where(r => r.Title.Equals(title) && r.AppUserId == userId) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 792728431..a9e78fe73 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -224,6 +224,7 @@ public class UserRepository : IUserRepository { return await _context.AppUserBookmark .Where(b => bookmarkIds.Contains(b.Id)) + .OrderBy(b => b.Created) .ToListAsync(); } diff --git a/API/Entities/AppUserBookmark.cs b/API/Entities/AppUserBookmark.cs index 81a26a08b..6d1ff0450 100644 --- a/API/Entities/AppUserBookmark.cs +++ b/API/Entities/AppUserBookmark.cs @@ -1,11 +1,13 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; +using API.Entities.Interfaces; namespace API.Entities { /// /// Represents a saved page in a Chapter entity for a given user. /// - public class AppUserBookmark + public class AppUserBookmark : IEntityDate { public int Id { get; set; } public int Page { get; set; } @@ -23,5 +25,7 @@ namespace API.Entities [JsonIgnore] public AppUser AppUser { get; set; } public int AppUserId { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } } } diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 043e5c919..5cc4718bb 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using System.Threading.Tasks; using API.Constants; using API.Data; @@ -32,6 +33,11 @@ namespace API.Extensions opt.Password.RequiredLength = 6; opt.SignIn.RequireConfirmedEmail = true; + + opt.Lockout.AllowedForNewUsers = true; + opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10); + opt.Lockout.MaxFailedAccessAttempts = 5; + }) .AddTokenProvider>(TokenOptions.DefaultProvider) .AddRoles() diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index c5d947b26..b79ad0889 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -276,6 +276,10 @@ namespace API.Parser new Regex( @"^(?!Vol)(?.*)( |_|-)(ch?)\d+", MatchOptions, RegexTimeout), + // Japanese Volume: n巻 -> Volume n + new Regex( + @"(?.+?)第(?\d+(?:(\-)\d+)?)巻", + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicSeriesRegex = new[] diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index c453240c4..03d589776 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -28,6 +28,8 @@ public interface IImageService /// Where to output the file /// File of written webp image Task ConvertToWebP(string filePath, string outputPath); + + Task IsImage(string filePath); } public class ImageService : IImageService @@ -117,6 +119,23 @@ public class ImageService : IImageService return outputFile; } + public async Task IsImage(string filePath) + { + try + { + var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath); + if (info == null) return false; + + return true; + } + catch (Exception ex) + { + /* Swallow Exception */ + } + + return false; + } + /// public string CreateThumbnailFromBase64(string encodedImage, string fileName) diff --git a/README.md b/README.md index e4f47b795..b92725d04 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ your reading collection with your friends and family! - [x] Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar, 7zip, raw images) and Books (epub, pdf) - [x] First class responsive readers that work great on any device (phone, tablet, desktop) - [x] Dark mode and customizable theming support -- [ ] Provide hooks into metadata providers to fetch metadata for Comics, Manga, and Books +- [ ] Provide a plugin system to allow external metadata integration and scrobbling for read status, ratings, and reviews - [x] Metadata should allow for collections, want to read integration from 3rd party services, genres. - [x] Ability to manage users, access, and ratings -- [ ] Ability to sync ratings and reviews to external services - [x] Fully Accessible with active accessibility audits - [x] Dedicated webtoon reading mode +- [ ] Full localization support - [ ] And so much [more...](https://github.com/Kareadita/Kavita/projects) ## Support @@ -93,6 +93,9 @@ Thank you to [ JetBrains](http: ## Palace-Designs We would like to extend a big thank you to [](https://www.palace-designs.com/) who hosts our infrastructure pro-bono. +## Huntr +We would like to extend a big thank you to [Huntr](https://huntr.dev/repos/kareadita/kavita) who has worked with Kavita in reporting security vulnerabilities. If you are interested in +being paid to help secure Kavita, please give them a try. ### License diff --git a/SECURITY.md b/SECURITY.md index ecf66fe24..56f010fb3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,10 +2,9 @@ ## Supported Versions -Security is maintained on latest stable version only. +Security is maintained on latest stable version only. ## Reporting a Vulnerability -Use this section to tell people how to report a vulnerability. -Please reach out via majora2007@users.noreply.github.com or via our discord (majora2007) +Please reach out to majora2007 via our Discord or you can (and should) report your vulnerability via [Huntr](https://huntr.dev/repos/kareadita/kavita). diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 18b8666b8..fef8d479d 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -267,6 +267,25 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "terser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } } } }, @@ -1787,23 +1806,23 @@ } }, "@angular-devkit/architect": { - "version": "0.1303.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.7.tgz", - "integrity": "sha512-xr35v7AuJygRdiaFhgoBSLN2ZMUri8x8Qx9jkmCkD3WLKz33TSFyAyqwdNNmOO9riK8ePXMH/QcSv0wY12pFBw==", + "version": "0.1303.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.9.tgz", + "integrity": "sha512-RMHqCGDxbLqT+250A0a8vagsoTdqGjAxjhrvTeq7PJmClI7uJ/uA1Fs18+t85toIqVKn2hovdY9sNf42nBDD2Q==", "requires": { - "@angular-devkit/core": "13.3.7", + "@angular-devkit/core": "13.3.9", "rxjs": "6.6.7" } }, "@angular-devkit/build-angular": { - "version": "13.3.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.7.tgz", - "integrity": "sha512-XUmiq/3zpuna+r0UOqNSvA9kEcPwsLblEmNLUYyZXL9v/aGWUHOSH0nhGVrNRrSud4ryklEnxfkxkxlZlT4mjQ==", + "version": "13.3.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.9.tgz", + "integrity": "sha512-1LqcMizeabx3yOkx3tptCSAoEhG6nO6hPgI/B3EJ07G/ZcoxunMWSeN3P3zT10dZMEHhcxl+8cSStSXaXj9hfA==", "requires": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1303.7", - "@angular-devkit/build-webpack": "0.1303.7", - "@angular-devkit/core": "13.3.7", + "@angular-devkit/architect": "0.1303.9", + "@angular-devkit/build-webpack": "0.1303.9", + "@angular-devkit/core": "13.3.9", "@babel/core": "7.16.12", "@babel/generator": "7.16.8", "@babel/helper-annotate-as-pure": "7.16.7", @@ -1814,7 +1833,7 @@ "@babel/runtime": "7.16.7", "@babel/template": "7.16.7", "@discoveryjs/json-ext": "0.5.6", - "@ngtools/webpack": "13.3.7", + "@ngtools/webpack": "13.3.9", "ansi-colors": "4.1.1", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", @@ -1856,7 +1875,7 @@ "source-map-support": "0.5.21", "stylus": "0.56.0", "stylus-loader": "6.2.0", - "terser": "5.11.0", + "terser": "5.14.2", "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.3.1", @@ -1897,18 +1916,18 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.1303.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.7.tgz", - "integrity": "sha512-5vF399cPdwuCbzbxS4yNGgChdAzEM0/By21P0uiqBcIe/Zxuz3IUPapjvcyhkAo5OTu+d7smY9eusLHqoq1WFQ==", + "version": "0.1303.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.9.tgz", + "integrity": "sha512-CdYXvAN1xAik8FyfdF1B8Nt1B/1aBvkZr65AUVFOmP6wuVzcdn78BMZmZD42srYbV2449sWi5Vyo/j0a/lfJww==", "requires": { - "@angular-devkit/architect": "0.1303.7", + "@angular-devkit/architect": "0.1303.9", "rxjs": "6.6.7" } }, "@angular-devkit/core": { - "version": "13.3.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.7.tgz", - "integrity": "sha512-Ucy4bJmlgCoBenuVeGMdtW9dE8+cD+guWCgqexsFIG21KJ/l0ShZEZ/dGC1XibzaIs1HbKiTr/T1MOjInCV1rA==", + "version": "13.3.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.9.tgz", + "integrity": "sha512-XqCuIWyoqIsLABjV3GQL/+EiBCt3xVPPtNp3Mg4gjBsDLW7PEnvbb81yGkiZQmIsq4EIyQC/6fQa3VdjsCshGg==", "requires": { "ajv": "8.9.0", "ajv-formats": "2.1.1", @@ -1919,11 +1938,11 @@ } }, "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "requires": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.18.6" } }, "@babel/core": { @@ -1978,33 +1997,33 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" }, "@babel/highlight": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", - "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@jridgewell/trace-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", - "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "@ngtools/webpack": { - "version": "13.3.7", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.7.tgz", - "integrity": "sha512-KtNMHOGZIU2oaNTzk97ZNwTnJLbvnSpwyG3/+VW9xN92b2yw8gG9tHPKW2fsFrfzF9Mz8kqJeF31ftvkYuKtuA==" + "version": "13.3.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.9.tgz", + "integrity": "sha512-wmgOI5sogAuilwBZJqCHVMjm2uhDxjdSmNLFx7eznwGDa6LjvjuATqCv2dVlftq0Y/5oZFVrg5NpyHt5kfZ8Cg==" }, "@types/estree": { "version": "0.0.51", @@ -2054,9 +2073,9 @@ } }, "enhanced-resolve": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", - "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", + "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -2256,17 +2275,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, - "terser": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.11.0.tgz", - "integrity": "sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A==", - "requires": { - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - } - }, "webpack": { "version": "5.70.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", @@ -2854,6 +2862,36 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==" }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", @@ -3408,16 +3446,6 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, - "@types/yauzl": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", - "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -4344,12 +4372,6 @@ "ieee754": "^1.1.13" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4659,12 +4681,6 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5473,15 +5489,6 @@ } } }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "enhanced-resolve": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz", @@ -5908,29 +5915,6 @@ "tmp": "^0.0.33" } }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -6006,15 +5990,6 @@ "bser": "2.1.1" } }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, "fetch-cookie": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", @@ -8590,12 +8565,6 @@ } } }, - "jpeg-js": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", - "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9407,9 +9376,9 @@ } }, "ngx-extended-pdf-viewer": { - "version": "13.5.2", - "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-13.5.2.tgz", - "integrity": "sha512-dbGozWdfjHosHtJXRbM7zZQ8Zojdpv2/5e68767htvPRQ2JCUtRN+u6NwA59k+sNpNCliHhjaeFMXfWEWEHDMQ==", + "version": "14.5.3", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-14.5.3.tgz", + "integrity": "sha512-9pqnbonKcu/6SIwPe3yCfHzsO1fgO7qIwETHD7UuS2kAG5GM7VkEwrqMoF7qsZ0Lq/rkqFBcGsS4GYW5JK+oEQ==", "requires": { "lodash.deburr": "^4.1.0", "tslib": "^2.3.0" @@ -10110,12 +10079,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -10170,23 +10133,6 @@ "nice-napi": "^1.0.2" } }, - "pixelmatch": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", - "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", - "dev": true, - "requires": { - "pngjs": "^4.0.1" - }, - "dependencies": { - "pngjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", - "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", - "dev": true - } - } - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -10196,85 +10142,22 @@ } }, "playwright": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.20.2.tgz", - "integrity": "sha512-p6GE8A/f2G7t8FIk/AwQ94nT7R7tyPRJyKt1FwRjwBDf4WdpgoAr4hDfMgHy+CkClR22adFjopGwhxXAPsewhg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.24.2.tgz", + "integrity": "sha512-iMWDLgaFRT+7dXsNeYwgl8nhLHsUrzFyaRVC+ftr++P1dVs70mPrFKBZrGp1fOKigHV9d1syC03IpPbqLKlPsg==", "dev": true, "requires": { - "playwright-core": "1.20.2" + "playwright-core": "1.24.2" }, "dependencies": { - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true - }, "playwright-core": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.2.tgz", - "integrity": "sha512-iV6+HftSPalynkq0CYJala1vaTOq7+gU9BRfKCdM9bAxNq/lFLrwbluug2Wt5OoUwbMABcnTThIEm3/qUhCdJQ==", - "dev": true, - "requires": { - "colors": "1.4.0", - "commander": "8.3.0", - "debug": "4.3.3", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.0", - "jpeg-js": "0.4.3", - "mime": "3.0.0", - "pixelmatch": "5.2.1", - "pngjs": "6.0.0", - "progress": "2.0.3", - "proper-lockfile": "4.1.2", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "socks-proxy-agent": "6.1.1", - "stack-utils": "2.0.5", - "ws": "8.4.2", - "yauzl": "2.10.0", - "yazl": "2.5.1" - } - }, - "ws": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz", - "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.24.2.tgz", + "integrity": "sha512-zfAoDoPY/0sDLsgSgLZwWmSCevIg1ym7CppBwllguVBNiHeixZkc1AdMuYUPZC6AdEYc4CxWEyLMBTw2YcmRrA==", "dev": true } } }, - "pngjs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", - "dev": true - }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -10636,12 +10519,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -10675,25 +10552,6 @@ "sisteransi": "^1.0.5" } }, - "proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - }, - "dependencies": { - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - } - } - }, "protractor": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", @@ -10776,8 +10634,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "strip-ansi": { @@ -10982,8 +10839,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ansi-styles": { @@ -11053,12 +10909,6 @@ } } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -11070,16 +10920,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -12123,20 +11963,14 @@ } }, "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } } }, "terser-webpack-plugin": { @@ -13133,25 +12967,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==" }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3" - } - }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 965f267d2..2db8da812 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -39,7 +39,7 @@ "lazysizes": "^5.3.2", "ng-circle-progress": "^1.6.0", "ngx-color-picker": "^12.0.0", - "ngx-extended-pdf-viewer": "^13.5.2", + "ngx-extended-pdf-viewer": "^14.5.2", "ngx-file-drop": "^13.0.0", "ngx-infinite-scroll": "^13.0.2", "ngx-toastr": "^14.2.1", @@ -61,7 +61,7 @@ "jest": "^27.5.1", "jest-preset-angular": "^11.1.0", "karma-coverage": "~2.2.0", - "playwright": "^1.20.2", + "playwright": "^1.24.2", "protractor": "~7.0.0", "ts-node": "~10.5.0", "tslint": "^6.1.3", diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index ef53a0fdc..9223c57ac 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -290,6 +290,12 @@ export class ActionFactoryService { title: 'Add to Want To Read', callback: this.dummyCallback, requiresAdmin: false + }, + { + action: Action.RemoveFromWantToReadList, + title: 'Remove from Want To Read', + callback: this.dummyCallback, + requiresAdmin: false } ]; diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 2025517cd..94d268e77 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Injectable } from '@angular/core'; -import { NavigationStart, Router } from '@angular/router'; +import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; import { ReplaySubject } from 'rxjs'; import { filter } from 'rxjs/operators'; import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service'; @@ -23,6 +23,7 @@ export class BulkSelectionService { private selectedCards: { [key: string]: {[key: number]: boolean} } = {}; private dataSourceMax: { [key: string]: number} = {}; public isShiftDown: boolean = false; + private activeRoute: string = ''; private actionsSource = new ReplaySubject[]>(1); public actions$ = this.actionsSource.asObservable(); @@ -33,14 +34,16 @@ export class BulkSelectionService { */ public selections$ = this.selectionsSource.asObservable(); - constructor(private router: Router, private actionFactory: ActionFactoryService) { + constructor(private router: Router, private actionFactory: ActionFactoryService, private route: ActivatedRoute) { router.events .pipe(filter(event => event instanceof NavigationStart)) .subscribe((event) => { this.deselectAll(); this.dataSourceMax = {}; this.prevIndex = 0; + this.activeRoute = this.router.url; }); + } handleCardSelection(dataSource: DataSource, index: number, maxIndex: number, wasSelected: boolean) { @@ -143,7 +146,14 @@ export class BulkSelectionService { // else returns volume/chapter items const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection, Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList]; if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) { - return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action)); + let actions = this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action)); + if (this.activeRoute.startsWith('/want-to-read')) { + const removeFromWantToRead = {...actions[0]}; + removeFromWantToRead.action = Action.RemoveFromWantToReadList; + removeFromWantToRead.title = 'Remove from Want to Read'; + actions.push(removeFromWantToRead); + } + return actions; } if (Object.keys(this.selectedCards).filter(item => item === 'bookmark').length > 0) { diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 4061d0b50..9cf516bc8 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -117,7 +117,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges, // } // this.hasResumedJumpKey = true; // }); - console.log(this.noDataTemplate); } ngOnChanges(): void { diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index a592d684c..43fcb755c 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -33,9 +33,10 @@ -
+
-
+
@@ -64,7 +65,8 @@
+ 'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}" + [style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle"> @@ -214,6 +216,14 @@
+
+
+ + + {{generalSettingsForm.get('darkness')?.value + '%'}} +
+
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 9d2865186..4da92b8de 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -480,11 +480,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { autoCloseMenu: this.autoCloseMenu, pageSplitOption: this.pageSplitOption, fittingOption: this.translateScalingOption(this.scalingOption), - layoutMode: this.layoutMode + layoutMode: this.layoutMode, + darkness: 100 }); this.updateForm(); + this.generalSettingsForm.get('darkness')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { + console.log('brightness: ', val); + //this.cdRef.markForCheck(); + }); + this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { const changeOccurred = parseInt(val, 10) !== this.layoutMode; diff --git a/UI/Web/src/app/manga-reader/manga-reader.module.ts b/UI/Web/src/app/manga-reader/manga-reader.module.ts index 214962509..03abe09e1 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.module.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.module.ts @@ -9,6 +9,7 @@ import { NgxSliderModule } from '@angular-slider/ngx-slider'; import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component'; import { ReaderSharedModule } from '../reader-shared/reader-shared.module'; import { FullscreenIconPipe } from './fullscreen-icon.pipe'; +import { PipeModule } from '../pipe/pipe.module'; @NgModule({ declarations: [ @@ -20,6 +21,7 @@ import { FullscreenIconPipe } from './fullscreen-icon.pipe'; CommonModule, MangaReaderRoutingModule, ReactiveFormsModule, + PipeModule, NgbDropdownModule, NgxSliderModule, diff --git a/UI/Web/src/app/pipe/pipe.module.ts b/UI/Web/src/app/pipe/pipe.module.ts index ac30d38e7..bd907a8ff 100644 --- a/UI/Web/src/app/pipe/pipe.module.ts +++ b/UI/Web/src/app/pipe/pipe.module.ts @@ -13,6 +13,7 @@ import { AgeRatingPipe } from './age-rating.pipe'; import { MangaFormatPipe } from './manga-format.pipe'; import { MangaFormatIconPipe } from './manga-format-icon.pipe'; import { LibraryTypePipe } from './library-type.pipe'; +import { SafeStylePipe } from './safe-style.pipe'; @@ -30,7 +31,8 @@ import { LibraryTypePipe } from './library-type.pipe'; AgeRatingPipe, MangaFormatPipe, MangaFormatIconPipe, - LibraryTypePipe + LibraryTypePipe, + SafeStylePipe ], imports: [ CommonModule, @@ -48,7 +50,8 @@ import { LibraryTypePipe } from './library-type.pipe'; AgeRatingPipe, MangaFormatPipe, MangaFormatIconPipe, - LibraryTypePipe + LibraryTypePipe, + SafeStylePipe ] }) export class PipeModule { } diff --git a/UI/Web/src/app/pipe/safe-style.pipe.ts b/UI/Web/src/app/pipe/safe-style.pipe.ts new file mode 100644 index 000000000..3b20b9944 --- /dev/null +++ b/UI/Web/src/app/pipe/safe-style.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Pipe({ + name: 'safeStyle' +}) +export class SafeStylePipe implements PipeTransform { + + constructor(private sanitizer: DomSanitizer){ + } + + transform(style: string) { + return this.sanitizer.bypassSecurityTrustStyle(style); + } + +} diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts index 9603405b7..06af0998e 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts @@ -49,7 +49,6 @@ export class SideNavComponent implements OnInit, OnDestroy { } }); }); - } ngOnInit(): void { diff --git a/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts b/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts index e1bdf3608..da748de4d 100644 --- a/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts +++ b/UI/Web/src/app/want-to-read/want-to-read/want-to-read.component.ts @@ -54,6 +54,7 @@ export class WantToReadComponent implements OnInit, OnDestroy { case Action.RemoveFromWantToReadList: this.actionService.removeMultipleSeriesFromWantToReadList(selectedSeries.map(s => s.id), () => { this.bulkSelectionService.deselectAll(); + this.loadPage(); this.cdRef.markForCheck(); }); break;