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