Merge pull request #49 from Kareadita/feature/settings-continued

User Preferences
This commit is contained in:
Joseph Milazzo 2021-02-07 12:10:13 -06:00 committed by GitHub
commit b30560fdda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1045 additions and 38 deletions

View File

@ -1,18 +1,10 @@
using API.Helpers.Converters; using API.Helpers.Converters;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Converters namespace API.Tests.Converters
{ {
public class CronConverterTests public class CronConverterTests
{ {
private readonly ITestOutputHelper _testOutputHelper;
public CronConverterTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Theory] [Theory]
[InlineData("daily", "0 0 * * *")] [InlineData("daily", "0 0 * * *")]
[InlineData("disabled", "0 0 31 2 *")] [InlineData("disabled", "0 0 31 2 *")]

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities; using API.Entities.Enums;
using API.Parser; using API.Parser;
using Xunit; using Xunit;
using static API.Parser.Parser; using static API.Parser.Parser;
@ -109,7 +109,10 @@ namespace API.Tests
[InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")] [InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")]
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")]
[InlineData("Yumekui-Merry DKThiasScanlations Chapter51v2", "51")]
[InlineData("Goblin Slayer Side Story - Year One 017.5", "17.5")]
[InlineData("Beelzebub_53[KSH].zip", "53")] [InlineData("Beelzebub_53[KSH].zip", "53")]
[InlineData("Black Bullet - v4 c20.5 [batoto]", "20.5")]
//[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")] //[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {

View File

@ -1,8 +1,4 @@
using API.Interfaces; using Xunit;
using API.Services;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Services namespace API.Tests.Services
{ {

View File

@ -63,6 +63,7 @@ namespace API.Controllers
} }
var user = _mapper.Map<AppUser>(registerDto); var user = _mapper.Map<AppUser>(registerDto);
user.UserPreferences ??= new AppUserPreferences();
var result = await _userManager.CreateAsync(user, registerDto.Password); var result = await _userManager.CreateAsync(user, registerDto.Password);
@ -83,13 +84,14 @@ namespace API.Controllers
lib.AppUsers ??= new List<AppUser>(); lib.AppUsers ??= new List<AppUser>();
lib.AppUsers.Add(user); lib.AppUsers.Add(user);
} }
if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually."); if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually.");
} }
return new UserDto return new UserDto
{ {
Username = user.UserName, Username = user.UserName,
Token = await _tokenService.CreateToken(user), Token = await _tokenService.CreateToken(user),
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
}; };
} }
@ -97,11 +99,12 @@ namespace API.Controllers
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto) public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
{ {
var user = await _userManager.Users var user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
var debugUsers = await _userManager.Users.Select(x => x.NormalizedUserName).ToListAsync(); var debugUsers = await _userManager.Users.Select(x => x.NormalizedUserName).ToListAsync();
_logger.LogInformation($"All Users: {String.Join(",", debugUsers)}"); _logger.LogInformation($"All Users: {string.Join(",", debugUsers)}");
if (user == null) return Unauthorized("Invalid username"); if (user == null) return Unauthorized("Invalid username");
@ -112,6 +115,8 @@ namespace API.Controllers
// Update LastActive on account // Update LastActive on account
user.LastActive = DateTime.Now; user.LastActive = DateTime.Now;
user.UserPreferences ??= new AppUserPreferences();
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
await _unitOfWork.Complete(); await _unitOfWork.Complete();
@ -120,7 +125,8 @@ namespace API.Controllers
return new UserDto return new UserDto
{ {
Username = user.UserName, Username = user.UserName,
Token = await _tokenService.CreateToken(user) Token = await _tokenService.CreateToken(user),
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
}; };
} }
} }

View File

@ -0,0 +1,30 @@
using API.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace API.Controllers
{
[Authorize(Policy = "RequireAdminRole")]
public class ServerController : BaseApiController
{
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<ServerController> _logger;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
}
[HttpPost("restart")]
public ActionResult RestartServer()
{
_logger.LogInformation($"{User.GetUsername()} is restarting server from admin dashboard.");
_applicationLifetime.StopApplication();
return Ok();
}
}
}

View File

@ -1,8 +1,9 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Helpers.Converters; using API.Helpers.Converters;
using API.Interfaces; using API.Interfaces;
@ -62,6 +63,13 @@ namespace API.Controllers
setting.Value = updateSettingsDto.TaskScan; setting.Value = updateSettingsDto.TaskScan;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + "" != setting.Value)
{
setting.Value = updateSettingsDto.Port + "";
Environment.SetEnvironmentVariable("KAVITA_PORT", setting.Value);
_unitOfWork.SettingsRepository.Update(setting);
}
} }
if (_unitOfWork.HasChanges() && await _unitOfWork.Complete()) if (_unitOfWork.HasChanges() && await _unitOfWork.Complete())

View File

@ -44,5 +44,25 @@ namespace API.Controllers
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()); var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
return Ok(libs.Any(x => x.Id == libraryId)); return Ok(libs.Any(x => x.Id == libraryId));
} }
[HttpPost("update-preferences")]
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
{
var existingPreferences = await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername());
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.HideReadOnDetails = preferencesDto.HideReadOnDetails;
_unitOfWork.UserRepository.Update(existingPreferences);
if (await _unitOfWork.Complete())
{
return Ok(preferencesDto);
}
return BadRequest("There was an issue saving preferences.");
}
} }
} }

View File

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Entities; using API.Entities.Enums;
namespace API.DTOs namespace API.DTOs
{ {

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities; using API.Entities.Enums;
namespace API.DTOs namespace API.DTOs
{ {

View File

@ -1,4 +1,4 @@
using API.Entities; using API.Entities.Enums;
namespace API.DTOs namespace API.DTOs
{ {

View File

@ -6,5 +6,6 @@
public string TaskScan { get; set; } public string TaskScan { get; set; }
public string LoggingLevel { get; set; } public string LoggingLevel { get; set; }
public string TaskBackup { get; set; } public string TaskBackup { get; set; }
public int Port { get; set; }
} }
} }

View File

@ -1,8 +1,10 @@
namespace API.DTOs 
namespace API.DTOs
{ {
public class UserDto public class UserDto
{ {
public string Username { get; init; } public string Username { get; init; }
public string Token { get; init; } public string Token { get; init; }
public UserPreferencesDto Preferences { get; set; }
} }
} }

View File

@ -0,0 +1,15 @@
using API.Entities.Enums;
namespace API.DTOs
{
public class UserPreferencesDto
{
public ReadingDirection ReadingDirection { get; set; }
public ScalingOption ScalingOption { get; set; }
public PageSplitOption PageSplitOption { get; set; }
/// <summary>
/// Whether UI hides read Volumes on Details page
/// </summary>
public bool HideReadOnDetails { get; set; }
}
}

View File

@ -1,4 +1,5 @@
 
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace API.DTOs namespace API.DTOs
@ -11,6 +12,9 @@ namespace API.DTOs
public byte[] CoverImage { get; set; } public byte[] CoverImage { get; set; }
public int Pages { get; set; } public int Pages { get; set; }
public int PagesRead { get; set; } public int PagesRead { get; set; }
public DateTime LastModified { get; set; }
public DateTime Created { get; set; }
public bool IsSpecial { get; set; }
public ICollection<ChapterDto> Chapters { get; set; } public ICollection<ChapterDto> Chapters { get; set; }
} }
} }

View File

@ -28,6 +28,7 @@ namespace API.Data
public DbSet<AppUserProgress> AppUserProgresses { get; set; } public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; } public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; } public DbSet<ServerSetting> ServerSetting { get; set; }
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {

View File

@ -0,0 +1,718 @@
// <auto-generated />
using System;
using API.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20210205220227_UserPreferences")]
partial class UserPreferences
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<bool>("HideReadOnDetails")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");
b.Property<int>("ReadingDirection")
.HasColumnType("INTEGER");
b.Property<int>("ScalingOption")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId")
.IsUnique();
b.ToTable("AppUserPreferences");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PagesRead")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserProgresses");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("Review")
.HasColumnType("TEXT");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserRating");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Number")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("Range")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("FolderPath");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<int>("NumberOfPages")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("MangaFile");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("OriginalName")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("SortName")
.HasColumnType("TEXT");
b.Property<string>("Summary")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("Series");
});
modelBuilder.Entity("API.Entities.ServerSetting", b =>
{
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSetting");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<bool>("IsSpecial")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SeriesId");
b.ToTable("Volume");
});
modelBuilder.Entity("AppUserLibrary", b =>
{
b.Property<int>("AppUsersId")
.HasColumnType("INTEGER");
b.Property<int>("LibrariesId")
.HasColumnType("INTEGER");
b.HasKey("AppUsersId", "LibrariesId");
b.HasIndex("LibrariesId");
b.ToTable("AppUserLibrary");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithOne("UserPreferences")
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Progresses")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Ratings")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.HasOne("API.Entities.AppRole", "Role")
.WithMany("UserRoles")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.AppUser", "User")
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.HasOne("API.Entities.Library", "Library")
.WithMany("Folders")
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany("Files")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.HasOne("API.Entities.Library", "Library")
.WithMany("Series")
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.HasOne("API.Entities.Series", "Series")
.WithMany("Volumes")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("AppUserLibrary", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("AppUsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Library", null)
.WithMany()
.HasForeignKey("LibrariesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.HasOne("API.Entities.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Navigation("Progresses");
b.Navigation("Ratings");
b.Navigation("UserPreferences");
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Navigation("Folders");
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Navigation("Volumes");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class UserPreferences : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppUserPreferences",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ReadingDirection = table.Column<int>(type: "INTEGER", nullable: false),
ScalingOption = table.Column<int>(type: "INTEGER", nullable: false),
PageSplitOption = table.Column<int>(type: "INTEGER", nullable: false),
HideReadOnDetails = table.Column<bool>(type: "INTEGER", nullable: false),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserPreferences", x => x.Id);
table.ForeignKey(
name: "FK_AppUserPreferences_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppUserPreferences_AppUserId",
table: "AppUserPreferences",
column: "AppUserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserPreferences");
}
}
}

View File

@ -118,6 +118,35 @@ namespace API.Data.Migrations
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<bool>("HideReadOnDetails")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");
b.Property<int>("ReadingDirection")
.HasColumnType("INTEGER");
b.Property<int>("ScalingOption")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId")
.IsUnique();
b.ToTable("AppUserPreferences");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b => modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -486,6 +515,17 @@ namespace API.Data.Migrations
b.ToTable("AspNetUserTokens"); b.ToTable("AspNetUserTokens");
}); });
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithOne("UserPreferences")
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b => modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{ {
b.HasOne("API.Entities.AppUser", "AppUser") b.HasOne("API.Entities.AppUser", "AppUser")
@ -644,6 +684,8 @@ namespace API.Data.Migrations
b.Navigation("Ratings"); b.Navigation("Ratings");
b.Navigation("UserPreferences");
b.Navigation("UserRoles"); b.Navigation("UserRoles");
}); });

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Entities; using API.Entities;
using API.Entities.Enums;
using API.Services; using API.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -30,12 +31,15 @@ namespace API.Data
public static async Task SeedSettings(DataContext context) public static async Task SeedSettings(DataContext context)
{ {
context.Database.EnsureCreated(); await context.Database.EnsureCreatedAsync();
IList<ServerSetting> defaultSettings = new List<ServerSetting>() IList<ServerSetting> defaultSettings = new List<ServerSetting>()
{ {
new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory}, new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory},
new () {Key = ServerSettingKey.TaskScan, Value = "daily"} new () {Key = ServerSettingKey.TaskScan, Value = "daily"},
//new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"},
//new () {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new () {Key = ServerSettingKey.Port, Value = "5000"},
}; };
foreach (var defaultSetting in defaultSettings) foreach (var defaultSetting in defaultSettings)
@ -43,7 +47,7 @@ namespace API.Data
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key); var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
if (existing == null) if (existing == null)
{ {
context.ServerSetting.Add(defaultSetting); await context.ServerSetting.AddAsync(defaultSetting);
} }
} }

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums;
using API.Interfaces; using API.Interfaces;
using AutoMapper; using AutoMapper;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@ -26,6 +26,11 @@ namespace API.Data
_context.Entry(user).State = EntityState.Modified; _context.Entry(user).State = EntityState.Modified;
} }
public void Update(AppUserPreferences preferences)
{
_context.Entry(preferences).State = EntityState.Modified;
}
public void Delete(AppUser user) public void Delete(AppUser user)
{ {
_context.AppUser.Remove(user); _context.AppUser.Remove(user);
@ -59,6 +64,13 @@ namespace API.Data
_context.AppUserRating.Add(userRating); _context.AppUserRating.Add(userRating);
} }
public async Task<AppUserPreferences> GetPreferencesAsync(string username)
{
return await _context.AppUserPreferences
.Include(p => p.AppUser)
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync() public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{ {
return await _context.Users return await _context.Users

View File

@ -15,6 +15,7 @@ namespace API.Entities
public ICollection<AppUserRole> UserRoles { get; set; } public ICollection<AppUserRole> UserRoles { get; set; }
public ICollection<AppUserProgress> Progresses { get; set; } public ICollection<AppUserProgress> Progresses { get; set; }
public ICollection<AppUserRating> Ratings { get; set; } public ICollection<AppUserRating> Ratings { get; set; }
public AppUserPreferences UserPreferences { get; set; }
[ConcurrencyCheck] [ConcurrencyCheck]
public uint RowVersion { get; set; } public uint RowVersion { get; set; }

View File

@ -0,0 +1,21 @@
using API.Entities.Enums;
namespace API.Entities
{
public class AppUserPreferences
{
public int Id { get; set; }
public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight;
public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight;
public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft;
/// <summary>
/// Whether UI hides read Volumes on Details page
/// </summary>
public bool HideReadOnDetails { get; set; } = false;
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
namespace API.Entities namespace API.Entities.Enums
{ {
public enum LibraryType public enum LibraryType
{ {

View File

@ -1,6 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
namespace API.Entities namespace API.Entities.Enums
{ {
public enum MangaFormat public enum MangaFormat
{ {

View File

@ -0,0 +1,9 @@
namespace API.Entities.Enums
{
public enum PageSplitOption
{
SplitLeftToRight = 0,
SplitRightToLeft = 1,
NoSplit = 2
}
}

View File

@ -0,0 +1,8 @@
namespace API.Entities.Enums
{
public enum ReadingDirection
{
LeftToRight = 0,
RightToLeft = 1
}
}

View File

@ -0,0 +1,9 @@
namespace API.Entities.Enums
{
public enum ScalingOption
{
FitToHeight = 0,
FitToWidth = 1,
Original = 2
}
}

View File

@ -1,10 +1,11 @@
namespace API.Entities namespace API.Entities.Enums
{ {
public enum ServerSettingKey public enum ServerSettingKey
{ {
TaskScan = 0, TaskScan = 0,
CacheDirectory = 1, CacheDirectory = 1,
TaskBackup = 2, TaskBackup = 2,
LoggingLevel = 3 LoggingLevel = 3,
Port = 4
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
namespace API.Entities namespace API.Entities

View File

@ -1,4 +1,6 @@
 
using API.Entities.Enums;
namespace API.Entities namespace API.Entities
{ {
public class MangaFile public class MangaFile

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
namespace API.Entities namespace API.Entities

View File

@ -21,6 +21,8 @@ namespace API.Helpers
CreateMap<Series, SeriesDto>(); CreateMap<Series, SeriesDto>();
CreateMap<AppUserPreferences, UserPreferencesDto>();
CreateMap<Library, LibraryDto>() CreateMap<Library, LibraryDto>()
.ForMember(dest => dest.Folders, .ForMember(dest => dest.Folders,
opt => opt =>

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums;
using AutoMapper; using AutoMapper;
namespace API.Helpers.Converters namespace API.Helpers.Converters
@ -26,7 +27,9 @@ namespace API.Helpers.Converters
case ServerSettingKey.TaskBackup: case ServerSettingKey.TaskBackup:
destination.TaskBackup = row.Value; destination.TaskBackup = row.Value;
break; break;
case ServerSettingKey.Port:
destination.Port = int.Parse(row.Value);
break;
} }
} }

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums;
namespace API.Interfaces namespace API.Interfaces
{ {

View File

@ -8,11 +8,13 @@ namespace API.Interfaces
public interface IUserRepository public interface IUserRepository
{ {
void Update(AppUser user); void Update(AppUser user);
void Update(AppUserPreferences preferences);
public void Delete(AppUser user); public void Delete(AppUser user);
Task<AppUser> GetUserByUsernameAsync(string username); Task<AppUser> GetUserByUsernameAsync(string username);
Task<IEnumerable<MemberDto>> GetMembersAsync(); Task<IEnumerable<MemberDto>> GetMembersAsync();
Task<IEnumerable<AppUser>> GetAdminUsersAsync(); Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<AppUserRating> GetUserRating(int seriesId, int userId); Task<AppUserRating> GetUserRating(int seriesId, int userId);
void AddRatingTracking(AppUserRating userRating); void AddRatingTracking(AppUserRating userRating);
Task<AppUserPreferences> GetPreferencesAsync(string username);
} }
} }

View File

@ -1,9 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.Entities; using API.Entities.Enums;
namespace API.Parser namespace API.Parser
{ {

View File

@ -1,5 +1,4 @@
 using API.Entities.Enums;
using API.Entities;
namespace API.Parser namespace API.Parser
{ {

View File

@ -13,6 +13,7 @@ namespace API
{ {
public class Program public class Program
{ {
private static readonly int HttpPort = 5000;
protected Program() protected Program()
{ {
} }
@ -46,7 +47,40 @@ namespace API
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => .ConfigureWebHostDefaults(webBuilder =>
{ {
webBuilder.UseKestrel((opts) =>
{
opts.ListenAnyIP(HttpPort);
});
webBuilder.UseStartup<Startup>(); webBuilder.UseStartup<Startup>();
}); });
// private static void StartNewInstance()
// {
// //_logger.LogInformation("Starting new instance");
//
// var module = options.RestartPath;
//
// if (string.IsNullOrWhiteSpace(module))
// {
// module = Environment.GetCommandLineArgs()[0];
// }
//
// // string commandLineArgsString;
// // if (options.RestartArgs != null)
// // {
// // commandLineArgsString = options.RestartArgs ?? string.Empty;
// // }
// // else
// // {
// // commandLineArgsString = string.Join(
// // ' ',
// // Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument));
// // }
//
// //_logger.LogInformation("Executable: {0}", module);
// //_logger.LogInformation("Arguments: {0}", commandLineArgsString);
//
// Process.Start(module, Array.Empty<string>);
// }
} }
} }

View File

@ -129,6 +129,7 @@ namespace API.Services
try { try {
// TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images) // TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images)
// or we need to move this filtering to another area (Process) // or we need to move this filtering to another area (Process)
// or we can get all files and put a check in place during Process to abandon files
files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions) files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions)
.ToArray(); .ToArray();
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Entities; using API.Entities;
using API.Entities.Enums;
using API.Interfaces; using API.Interfaces;
using API.Parser; using API.Parser;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -1,5 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Entities; using API.Entities.Enums;
using API.Helpers.Converters; using API.Helpers.Converters;
using API.Interfaces; using API.Interfaces;
using Hangfire; using Hangfire;

View File

@ -3,6 +3,7 @@ using API.Middleware;
using Hangfire; using Hangfire;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -26,6 +27,11 @@ namespace API
services.AddApplicationServices(_config); services.AddApplicationServices(_config);
services.AddControllers(); services.AddControllers();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
services.AddCors(); services.AddCors();
services.AddIdentityServices(_config); services.AddIdentityServices(_config);
services.AddSwaggerGen(c => services.AddSwaggerGen(c =>
@ -46,10 +52,15 @@ namespace API
app.UseHangfireDashboard(); app.UseHangfireDashboard();
} }
app.UseForwardedHeaders();
app.UseRouting(); app.UseRouting();
// Ordering is important. Cors, authentication, authorization // Ordering is important. Cors, authentication, authorization
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); if (env.IsDevelopment())
{
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200"));
}
app.UseAuthentication(); app.UseAuthentication();
@ -62,6 +73,7 @@ namespace API
ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default
}); });
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();