Security Hotfix (#1415)

* Updated ngx-extended-pdf-viewer to 14.5.2 + misc security vuln

* Hooked up remove from want to read AND fixed a bug in the logic that was removing everything BUT what was passed.

Allow for bookmarks to have date info for better ordering.

* Implemented a quick way to set darkneses level on manga reader for when nightlight just isn't dark enough

* Added Japanese Series name support in the Parser

* Updated our security file with our Huntr.

* Fixed a security vulnerability where through the API, an unauthorized user could delete/modify reading lists that did not belong to them.

Fixed a bug where when creating a reading list with the name of another users, the API would throw an exception (but reading list would still get created)

* Ensure all reading list apis are authorized

* Ensured all APIs require authentication, except those that explicitly don't. All APIs are default requiring Authentication.

Fixed a security vulnerability which would allow a user to take over an admin account.

* Fixed a bug where cover-upload would accept filenames that were not expected.

* Explicitly check that a user has access to the pdf file before we serve it back.

* Enabled lock out when invalid user auth occurs. After 5 invalid auths, the user account will be locked out for 10 mins.
This commit is contained in:
Joseph Milazzo 2022-08-08 15:47:37 -05:00 committed by GitHub
parent 331e0d0ca9
commit 88b5ebeb69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1988 additions and 358 deletions

View File

@ -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));

View File

@ -70,13 +70,21 @@ namespace API.Controllers
/// </summary>
/// <param name="resetPasswordDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("reset-password")]
public async Task<ActionResult> 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
/// </summary>
/// <param name="registerDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("register")]
public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
{
@ -158,6 +167,7 @@ namespace API.Controllers
/// </summary>
/// <param name="loginDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<UserDto>> 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
/// </summary>
/// <param name="tokenRequestDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("refresh-token")]
public async Task<ActionResult<TokenRequestDto>> 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<ActionResult<UserDto>> ConfirmEmail(ConfirmEmailDto dto)
{

View File

@ -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.
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("exists")]
public async Task<ActionResult<bool>> AdminExists()
{

View File

@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class BaseApiController : ControllerBase
{
}

View File

@ -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");
}
}

View File

@ -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(".", "");

View File

@ -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;

View File

@ -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);

View File

@ -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<AppUser?> 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;
}
/// <summary>
/// Updates an items position
/// </summary>
@ -84,6 +99,11 @@ namespace API.Controllers
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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));
}
/// <summary>
@ -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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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");

View File

@ -24,6 +24,7 @@ public class ThemeController : BaseApiController
_taskScheduler = taskScheduler;
}
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
{

View File

@ -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)

View File

@ -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();

File diff suppressed because it is too large Load Diff

View File

@ -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<DateTime>(
name: "Created",
table: "AppUserBookmark",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
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");
}
}
}

View File

@ -137,9 +137,15 @@ namespace API.Data.Migrations
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("Page")
.HasColumnType("INTEGER");

View File

@ -17,7 +17,7 @@ public interface IReadingListRepository
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId);
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title);
Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
@ -215,10 +215,10 @@ public class ReadingListRepository : IReadingListRepository
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
public async Task<ReadingListDto> 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<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
}

View File

@ -224,6 +224,7 @@ public class UserRepository : IUserRepository
{
return await _context.AppUserBookmark
.Where(b => bookmarkIds.Contains(b.Id))
.OrderBy(b => b.Created)
.ToListAsync();
}

View File

@ -1,11 +1,13 @@
using System.Text.Json.Serialization;
using System;
using System.Text.Json.Serialization;
using API.Entities.Interfaces;
namespace API.Entities
{
/// <summary>
/// Represents a saved page in a Chapter entity for a given user.
/// </summary>
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; }
}
}

View File

@ -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<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
.AddRoles<AppRole>()

View File

@ -276,6 +276,10 @@ namespace API.Parser
new Regex(
@"^(?!Vol)(?<Series>.*)( |_|-)(ch?)\d+",
MatchOptions, RegexTimeout),
// Japanese Volume: n巻 -> Volume n
new Regex(
@"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] ComicSeriesRegex = new[]

View File

@ -28,6 +28,8 @@ public interface IImageService
/// <param name="outputPath">Where to output the file</param>
/// <returns>File of written webp image</returns>
Task<string> ConvertToWebP(string filePath, string outputPath);
Task<bool> IsImage(string filePath);
}
public class ImageService : IImageService
@ -117,6 +119,23 @@ public class ImageService : IImageService
return outputFile;
}
public async Task<bool> 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;
}
/// <inheritdoc />
public string CreateThumbnailFromBase64(string encodedImage, string fileName)

View File

@ -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 [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
## Palace-Designs
We would like to extend a big thank you to [<img src="/Logo/hosting-sponsor.png" alt="" width="128">](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

View File

@ -6,6 +6,5 @@ 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).

395
UI/Web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
}
];

View File

@ -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<ActionItem<any>[]>(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) {

View File

@ -117,7 +117,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges,
// }
// this.hasResumedJumpKey = true;
// });
console.log(this.noDataTemplate);
}
ngOnChanges(): void {

View File

@ -33,9 +33,10 @@
</div>
</ng-container>
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
<div (click)="toggleMenu()" class="reading-area"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
<div class="image-container" [ngClass]="{'d-none': !renderWithCanvas }">
<div class="image-container" [ngClass]="{'d-none': !renderWithCanvas }" [style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)'">
<canvas #content class="{{getFittingOptionClass()}}"
ondragstart="return false;" onselectstart="return false;">
</canvas>
@ -64,7 +65,8 @@
<div class="image-container {{getFittingOptionClass()}}" [ngClass]="{'d-none': renderWithCanvas, 'center-double': ShouldRenderDoublePage,
'fit-to-width-double-offset' : FittingOption === FITTING_OPTION.WIDTH && ShouldRenderDoublePage,
'fit-to-height-double-offset': FittingOption === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage,
'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}">
'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}"
[style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle">
<img #image [src]="canvasImage.src" id="image-1"
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
@ -214,6 +216,14 @@
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-sm-12">
<label for="darkness" class="form-label range-label">Darkess</label>
<input type="range" class="form-range" id="darkness"
min="10" max="100" step="1" formControlName="darkness">
<span class="range-text">{{generalSettingsForm.get('darkness')?.value + '%'}}</span>
</div>
</div>
</form>
</div>
</div>

View File

@ -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;

View File

@ -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,

View File

@ -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 { }

View File

@ -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);
}
}

View File

@ -49,7 +49,6 @@ export class SideNavComponent implements OnInit, OnDestroy {
}
});
});
}
ngOnInit(): void {

View File

@ -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;