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 Xunit;
using Xunit.Abstractions;
namespace API.Tests.Converters
{
public class CronConverterTests
{
private readonly ITestOutputHelper _testOutputHelper;
public CronConverterTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Theory]
[InlineData("daily", "0 0 * * *")]
[InlineData("disabled", "0 0 31 2 *")]

View File

@ -1,5 +1,5 @@
using System.Collections.Generic;
using API.Entities;
using API.Entities.Enums;
using API.Parser;
using Xunit;
using static API.Parser.Parser;
@ -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)
{

View File

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

View File

@ -63,6 +63,7 @@ namespace API.Controllers
}
var user = _mapper.Map<AppUser>(registerDto);
user.UserPreferences ??= new AppUserPreferences();
var result = await _userManager.CreateAsync(user, registerDto.Password);
@ -83,13 +84,14 @@ namespace API.Controllers
lib.AppUsers ??= new List<AppUser>();
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<UserPreferencesDto>(user.UserPreferences)
};
}
@ -97,11 +99,12 @@ namespace API.Controllers
public async Task<ActionResult<UserDto>> 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 +115,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 +125,8 @@ namespace API.Controllers
return new UserDto
{
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.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
using API.Interfaces;
@ -62,6 +63,13 @@ namespace API.Controllers
setting.Value = updateSettingsDto.TaskScan;
_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())

View File

@ -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<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.ComponentModel.DataAnnotations;
using API.Entities;
using API.Entities.Enums;
namespace API.DTOs
{

View File

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

View File

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

View File

@ -6,5 +6,6 @@
public string TaskScan { get; set; }
public string LoggingLevel { 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 string Username { 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;
namespace API.DTOs
@ -11,6 +12,9 @@ namespace API.DTOs
public byte[] CoverImage { get; set; }
public int Pages { 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; }
}
}

View File

@ -28,6 +28,7 @@ namespace API.Data
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; }
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
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");
});
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")
@ -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");
});

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Entities;
using API.Entities.Enums;
using API.Services;
using Microsoft.AspNetCore.Identity;
@ -30,12 +31,15 @@ namespace API.Data
public static async Task SeedSettings(DataContext context)
{
context.Database.EnsureCreated();
await context.Database.EnsureCreatedAsync();
IList<ServerSetting> defaultSettings = new List<ServerSetting>()
{
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)
@ -43,7 +47,7 @@ namespace API.Data
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
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 API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Interfaces;
using AutoMapper;
using Microsoft.EntityFrameworkCore;

View File

@ -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<AppUserPreferences> GetPreferencesAsync(string username)
{
return await _context.AppUserPreferences
.Include(p => p.AppUser)
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{
return await _context.Users

View File

@ -15,6 +15,7 @@ namespace API.Entities
public ICollection<AppUserRole> UserRoles { get; set; }
public ICollection<AppUserProgress> Progresses { get; set; }
public ICollection<AppUserRating> Ratings { get; set; }
public AppUserPreferences UserPreferences { get; set; }
[ConcurrencyCheck]
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;
namespace API.Entities
namespace API.Entities.Enums
{
public enum LibraryType
{

View File

@ -1,6 +1,6 @@
using System.ComponentModel;
namespace API.Entities
namespace API.Entities.Enums
{
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
{
TaskScan = 0,
CacheDirectory = 1,
TaskBackup = 2,
LoggingLevel = 3
LoggingLevel = 3,
Port = 4
}
}

View File

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

View File

@ -1,4 +1,6 @@

using API.Entities.Enums;
namespace API.Entities
{
public class MangaFile

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@

using API.Entities;
using API.Entities.Enums;
namespace API.Parser
{

View File

@ -13,6 +13,7 @@ namespace API
{
public class Program
{
private static readonly int HttpPort = 5000;
protected Program()
{
}
@ -38,7 +39,7 @@ namespace API
var logger = services.GetRequiredService < ILogger<Program>>();
logger.LogError(ex, "An error occurred during migration");
}
await host.RunAsync();
}
@ -46,7 +47,40 @@ namespace API
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel((opts) =>
{
opts.ListenAnyIP(HttpPort);
});
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 {
// 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 can get all files and put a check in place during Process to abandon files
files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions)
.ToArray();
}

View File

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

View File

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

View File

@ -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<ForwardedHeadersOptions>(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 =>
{