diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 34b7798a3..21d73bdf2 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -109,7 +109,10 @@ namespace API.Tests [InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")] + [InlineData("Yumekui-Merry DKThiasScanlations Chapter51v2", "51")] + [InlineData("Goblin Slayer Side Story - Year One 017.5", "17.5")] [InlineData("Beelzebub_53[KSH].zip", "53")] + [InlineData("Black Bullet - v4 c20.5 [batoto]", "20.5")] //[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")] public void ParseChaptersTest(string filename, string expected) { diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 1406ce1e7..59fc1ed27 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Constants; +using API.Data.Migrations; using API.DTOs; using API.Entities; using API.Extensions; @@ -63,6 +64,7 @@ namespace API.Controllers } var user = _mapper.Map(registerDto); + user.UserPreferences ??= new AppUserPreferences(); var result = await _userManager.CreateAsync(user, registerDto.Password); @@ -83,13 +85,14 @@ namespace API.Controllers lib.AppUsers ??= new List(); lib.AppUsers.Add(user); } - if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually."); + if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually."); } return new UserDto { Username = user.UserName, Token = await _tokenService.CreateToken(user), + Preferences = _mapper.Map(user.UserPreferences) }; } @@ -97,11 +100,12 @@ namespace API.Controllers public async Task> Login(LoginDto loginDto) { var user = await _userManager.Users + .Include(u => u.UserPreferences) .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); var debugUsers = await _userManager.Users.Select(x => x.NormalizedUserName).ToListAsync(); - _logger.LogInformation($"All Users: {String.Join(",", debugUsers)}"); + _logger.LogInformation($"All Users: {string.Join(",", debugUsers)}"); if (user == null) return Unauthorized("Invalid username"); @@ -112,6 +116,8 @@ namespace API.Controllers // Update LastActive on account user.LastActive = DateTime.Now; + user.UserPreferences ??= new AppUserPreferences(); + _unitOfWork.UserRepository.Update(user); await _unitOfWork.Complete(); @@ -120,7 +126,8 @@ namespace API.Controllers return new UserDto { Username = user.UserName, - Token = await _tokenService.CreateToken(user) + Token = await _tokenService.CreateToken(user), + Preferences = _mapper.Map(user.UserPreferences) }; } } diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 36b173745..f8c81ffdd 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -1,5 +1,6 @@ using System.IO; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers { diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs new file mode 100644 index 000000000..1e40aa585 --- /dev/null +++ b/API/Controllers/ServerController.cs @@ -0,0 +1,30 @@ +using API.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + [Authorize(Policy = "RequireAdminRole")] + public class ServerController : BaseApiController + { + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly ILogger _logger; + + public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger) + { + _applicationLifetime = applicationLifetime; + _logger = logger; + } + + [HttpPost] + public ActionResult RestartServer() + { + _logger.LogInformation($"{User.GetUsername()} is restarting server from admin dashboard."); + + _applicationLifetime.StopApplication(); + return Ok(); + } + } +} \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 6a98eedd9..607f508e9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -44,5 +44,25 @@ namespace API.Controllers var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()); return Ok(libs.Any(x => x.Id == libraryId)); } + + [HttpPost("update-preferences")] + public async Task> UpdatePreferences(UserPreferencesDto preferencesDto) + { + var existingPreferences = await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()); + + existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; + existingPreferences.ScalingOption = preferencesDto.ScalingOption; + existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; + existingPreferences.HideReadOnDetails = preferencesDto.HideReadOnDetails; + + _unitOfWork.UserRepository.Update(existingPreferences); + + if (await _unitOfWork.Complete()) + { + return Ok(preferencesDto); + } + + return BadRequest("There was an issue saving preferences."); + } } } \ No newline at end of file diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index eb0bf6bf2..052fa226e 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -1,8 +1,10 @@ -namespace API.DTOs + +namespace API.DTOs { public class UserDto { public string Username { get; init; } public string Token { get; init; } + public UserPreferencesDto Preferences { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs new file mode 100644 index 000000000..bec209a5b --- /dev/null +++ b/API/DTOs/UserPreferencesDto.cs @@ -0,0 +1,15 @@ +using API.Entities.Enums; + +namespace API.DTOs +{ + public class UserPreferencesDto + { + public ReadingDirection ReadingDirection { get; set; } + public ScalingOption ScalingOption { get; set; } + public PageSplitOption PageSplitOption { get; set; } + /// + /// Whether UI hides read Volumes on Details page + /// + public bool HideReadOnDetails { get; set; } + } +} \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 6aea9a959..24a2dae47 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -28,6 +28,7 @@ namespace API.Data public DbSet AppUserProgresses { get; set; } public DbSet AppUserRating { get; set; } public DbSet ServerSetting { get; set; } + public DbSet AppUserPreferences { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs b/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs new file mode 100644 index 000000000..1bea7a402 --- /dev/null +++ b/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs @@ -0,0 +1,718 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210205220227_UserPreferences")] + partial class UserPreferences + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HideReadOnDetails") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210205220227_UserPreferences.cs b/API/Data/Migrations/20210205220227_UserPreferences.cs new file mode 100644 index 000000000..892eb9767 --- /dev/null +++ b/API/Data/Migrations/20210205220227_UserPreferences.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class UserPreferences : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppUserPreferences", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ReadingDirection = table.Column(type: "INTEGER", nullable: false), + ScalingOption = table.Column(type: "INTEGER", nullable: false), + PageSplitOption = table.Column(type: "INTEGER", nullable: false), + HideReadOnDetails = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserPreferences", x => x.Id); + table.ForeignKey( + name: "FK_AppUserPreferences_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserPreferences_AppUserId", + table: "AppUserPreferences", + column: "AppUserId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index fcbfaa084..c5066809c 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -118,6 +118,35 @@ namespace API.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HideReadOnDetails") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => { b.Property("Id") @@ -486,6 +515,17 @@ namespace API.Data.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -644,6 +684,8 @@ namespace API.Data.Migrations b.Navigation("Ratings"); + b.Navigation("UserPreferences"); + b.Navigation("UserRoles"); }); diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index a06be47ee..4f86ca5b5 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -25,6 +25,11 @@ namespace API.Data { _context.Entry(user).State = EntityState.Modified; } + + public void Update(AppUserPreferences preferences) + { + _context.Entry(preferences).State = EntityState.Modified; + } public void Delete(AppUser user) { @@ -59,6 +64,13 @@ namespace API.Data _context.AppUserRating.Add(userRating); } + public async Task GetPreferencesAsync(string username) + { + return await _context.AppUserPreferences + .Include(p => p.AppUser) + .SingleOrDefaultAsync(p => p.AppUser.UserName == username); + } + public async Task> GetMembersAsync() { return await _context.Users diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 3384df9ee..49df4d7af 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -15,6 +15,7 @@ namespace API.Entities public ICollection UserRoles { get; set; } public ICollection Progresses { get; set; } public ICollection Ratings { get; set; } + public AppUserPreferences UserPreferences { get; set; } [ConcurrencyCheck] public uint RowVersion { get; set; } diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs new file mode 100644 index 000000000..1a2e6b41b --- /dev/null +++ b/API/Entities/AppUserPreferences.cs @@ -0,0 +1,21 @@ +using API.Entities.Enums; + +namespace API.Entities +{ + public class AppUserPreferences + { + public int Id { get; set; } + public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; + public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight; + public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft; + /// + /// Whether UI hides read Volumes on Details page + /// + public bool HideReadOnDetails { get; set; } = false; + + + + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/LibraryType.cs b/API/Entities/Enums/LibraryType.cs similarity index 100% rename from API/Entities/LibraryType.cs rename to API/Entities/Enums/LibraryType.cs diff --git a/API/Entities/MangaFormat.cs b/API/Entities/Enums/MangaFormat.cs similarity index 100% rename from API/Entities/MangaFormat.cs rename to API/Entities/Enums/MangaFormat.cs diff --git a/API/Entities/Enums/PageSplitOption.cs b/API/Entities/Enums/PageSplitOption.cs new file mode 100644 index 000000000..ae44530c7 --- /dev/null +++ b/API/Entities/Enums/PageSplitOption.cs @@ -0,0 +1,9 @@ +namespace API.Entities.Enums +{ + public enum PageSplitOption + { + SplitLeftToRight = 0, + SplitRightToLeft = 1, + NoSplit = 2 + } +} \ No newline at end of file diff --git a/API/Entities/Enums/ReadingDirection.cs b/API/Entities/Enums/ReadingDirection.cs new file mode 100644 index 000000000..e702970c9 --- /dev/null +++ b/API/Entities/Enums/ReadingDirection.cs @@ -0,0 +1,8 @@ +namespace API.Entities.Enums +{ + public enum ReadingDirection + { + LeftToRight = 0, + RightToLeft = 1 + } +} \ No newline at end of file diff --git a/API/Entities/Enums/ScalingOption.cs b/API/Entities/Enums/ScalingOption.cs new file mode 100644 index 000000000..7e144100c --- /dev/null +++ b/API/Entities/Enums/ScalingOption.cs @@ -0,0 +1,9 @@ +namespace API.Entities.Enums +{ + public enum ScalingOption + { + FitToHeight = 0, + FitToWidth = 1, + Original = 2 + } +} \ No newline at end of file diff --git a/API/Entities/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs similarity index 100% rename from API/Entities/ServerSettingKey.cs rename to API/Entities/Enums/ServerSettingKey.cs diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 744fc08bb..4994cbb0d 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -21,6 +21,8 @@ namespace API.Helpers CreateMap(); + CreateMap(); + CreateMap() .ForMember(dest => dest.Folders, opt => diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 20f515350..6821da667 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -8,11 +8,13 @@ namespace API.Interfaces public interface IUserRepository { void Update(AppUser user); + void Update(AppUserPreferences preferences); public void Delete(AppUser user); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); Task> GetAdminUsersAsync(); Task GetUserRating(int seriesId, int userId); void AddRatingTracking(AppUserRating userRating); + Task GetPreferencesAsync(string username); } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index 129fbcdc2..af4dc1b8e 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -4,6 +4,7 @@ using API.Data; using API.Entities; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -17,6 +18,8 @@ namespace API { } + private static readonly int HttpPort = 5000; // TODO: Get from DB + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build(); @@ -46,7 +49,26 @@ namespace API Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseKestrel((builderContext, opts) => + { + opts.ListenAnyIP(HttpPort); + }); webBuilder.UseStartup(); }); + + private static string BuildUrl(string scheme, string bindAddress, int port) + { + return $"{scheme}://{bindAddress}:{port}"; + } + + private static void ConfigureKestrelForHttps(KestrelServerOptions options) + { + options.ListenAnyIP(HttpPort); + // options.ListenAnyIP(HttpsPort, listenOptions => + // { + // listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + // //listenOptions.UseHttps(pfxFilePath, pfxPassword); + // }); + } } } diff --git a/API/Startup.cs b/API/Startup.cs index 426616e51..d7a6f77f1 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -3,6 +3,7 @@ using API.Middleware; using Hangfire; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,11 @@ namespace API services.AddApplicationServices(_config); services.AddControllers(); + services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); services.AddCors(); services.AddIdentityServices(_config); services.AddSwaggerGen(c => @@ -45,11 +51,16 @@ namespace API app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1")); app.UseHangfireDashboard(); } + + app.UseForwardedHeaders(); app.UseRouting(); // Ordering is important. Cors, authentication, authorization - app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); + if (env.IsDevelopment()) + { + app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); + } app.UseAuthentication(); @@ -61,6 +72,7 @@ namespace API { ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default }); + app.UseEndpoints(endpoints => {