diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index d5e362c75..924242c9e 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using API.Constants; using API.DTOs; using API.Entities; +using API.Errors; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; @@ -39,18 +40,41 @@ namespace API.Controllers _logger = logger; _mapper = mapper; } - - [Authorize(Policy = "RequireAdminRole")] + [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName); + var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); + + if (resetPasswordDto.UserName != User.GetUsername() && !isAdmin) return Unauthorized("You are not permitted to this operation."); + + // Validate Password + foreach (var validator in _userManager.PasswordValidators) + { + var validationResult = await validator.ValidateAsync(_userManager, user, resetPasswordDto.Password); + if (!validationResult.Succeeded) + { + return BadRequest( + validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description))); + } + } + var result = await _userManager.RemovePasswordAsync(user); - if (!result.Succeeded) return BadRequest("Unable to update password"); + if (!result.Succeeded) + { + _logger.LogError("Could not update password"); + return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description))); + } + result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password); - if (!result.Succeeded) return BadRequest("Unable to update password"); + if (!result.Succeeded) + { + _logger.LogError("Could not update password"); + return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description))); + } _logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName); return Ok(); diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs new file mode 100644 index 000000000..c117abc9e --- /dev/null +++ b/API/Controllers/ImageController.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs; +using API.Extensions; +using API.Interfaces; +using API.Interfaces.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + public class ImageController : BaseApiController + { + private readonly IDirectoryService _directoryService; + private readonly ICacheService _cacheService; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + + public ImageController(IDirectoryService directoryService, ICacheService cacheService, + ILogger logger, IUnitOfWork unitOfWork) + { + _directoryService = directoryService; + _cacheService = cacheService; + _logger = logger; + _unitOfWork = unitOfWork; + } + + [HttpGet("chapter-cover")] + public async Task GetChapterCoverImage(int chapterId) + { + var content = await _unitOfWork.VolumeRepository.GetChapterCoverImageAsync(chapterId); + if (content == null) return BadRequest("No cover image"); + const string format = "jpeg"; + + Response.AddCacheHeader(content); + return File(content, "image/" + format); + } + + [HttpGet("volume-cover")] + public async Task GetVolumeCoverImage(int volumeId) + { + var content = await _unitOfWork.SeriesRepository.GetVolumeCoverImageAsync(volumeId); + if (content == null) return BadRequest("No cover image"); + const string format = "jpeg"; + + Response.AddCacheHeader(content); + return File(content, "image/" + format); + } + + [HttpGet("series-cover")] + public async Task GetSeriesCoverImage(int seriesId) + { + var content = await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId); + if (content == null) return BadRequest("No cover image"); + const string format = "jpeg"; + + Response.AddCacheHeader(content); + return File(content, "image/" + format); + } + } +} \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index e3f17ec3e..4b8c776df 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -228,10 +228,6 @@ namespace API.Controllers [HttpGet("search")] public async Task>> Search(string queryString) { - //NOTE: What about normalizing search query and only searching against normalizedname in Series? - // So One Punch would match One-Punch - // This also means less indexes we need. - // TODO: Add indexes of what we are searching on queryString = queryString.Replace(@"%", ""); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index c9e441cdb..21dce8411 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -40,12 +40,9 @@ namespace API.Controllers var content = await _directoryService.ReadFileAsync(path); var format = Path.GetExtension(path).Replace(".", ""); - - // Look into HttpContext.Cache so we can utilize a memorystream for Zip entries (want to limit response time by 300ms) + // Calculates SHA1 Hash for byte[] - using var sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider(); - Response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2")))); - Response.Headers.Add("Cache-Control", "private"); + Response.AddCacheHeader(content); return File(content, "image/" + format); } diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 883c65a40..2e0bbbfeb 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using API.Data; using API.DTOs; using API.Entities.Enums; using API.Extensions; @@ -9,6 +10,8 @@ using API.Helpers.Converters; using API.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace API.Controllers @@ -19,12 +22,14 @@ namespace API.Controllers private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly ITaskScheduler _taskScheduler; + private readonly IConfiguration _configuration; - public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler) + public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IConfiguration configuration) { _logger = logger; _unitOfWork = unitOfWork; _taskScheduler = taskScheduler; + _configuration = configuration; } [HttpGet("")] @@ -51,6 +56,9 @@ namespace API.Controllers // We do not allow CacheDirectory changes, so we will ignore. var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); + + var logLevelOptions = new LogLevelOptions(); + _configuration.GetSection("Logging:LogLevel").Bind(logLevelOptions); foreach (var setting in currentSettings) { @@ -72,8 +80,15 @@ namespace API.Controllers Environment.SetEnvironmentVariable("KAVITA_PORT", setting.Value); _unitOfWork.SettingsRepository.Update(setting); } + + if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + "" != setting.Value) + { + setting.Value = updateSettingsDto.LoggingLevel + ""; + _unitOfWork.SettingsRepository.Update(setting); + } } - + + _configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + ""; if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges() || !await _unitOfWork.Complete()) @@ -90,5 +105,12 @@ namespace API.Controllers { return Ok(CronConverter.Options); } + + [Authorize(Policy = "RequireAdminRole")] + [HttpGet("log-levels")] + public ActionResult> GetLogLevels() + { + return Ok(new string[] {"Trace", "Debug", "Information", "Warning", "Critical", "None"}); + } } } \ No newline at end of file diff --git a/API/Data/BookmarkDto.cs b/API/DTOs/BookmarkDto.cs similarity index 100% rename from API/Data/BookmarkDto.cs rename to API/DTOs/BookmarkDto.cs diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index ee58e6c18..00139a3b2 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -4,28 +4,27 @@ namespace API.DTOs { public class ChapterDto { - public int Id { get; set; } + public int Id { get; init; } /// /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// - public string Range { get; set; } + public string Range { get; init; } /// /// Smallest number of the Range. /// - public string Number { get; set; } - public byte[] CoverImage { get; set; } + public string Number { get; init; } /// /// Total number of pages in all MangaFiles /// - public int Pages { get; set; } + public int Pages { get; init; } /// /// The files that represent this Chapter /// - public ICollection Files { get; set; } + public ICollection Files { get; init; } /// /// Calculated at API time. Number of pages read for this Chapter for logged in user. /// public int PagesRead { get; set; } - public int VolumeId { get; set; } + public int VolumeId { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/CreateLibraryDto.cs b/API/DTOs/CreateLibraryDto.cs index f33430a6c..f9aa14639 100644 --- a/API/DTOs/CreateLibraryDto.cs +++ b/API/DTOs/CreateLibraryDto.cs @@ -7,11 +7,11 @@ namespace API.DTOs public class CreateLibraryDto { [Required] - public string Name { get; set; } + public string Name { get; init; } [Required] - public LibraryType Type { get; set; } + public LibraryType Type { get; init; } [Required] [MinLength(1)] - public IEnumerable Folders { get; set; } + public IEnumerable Folders { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/ImageDto.cs b/API/DTOs/ImageDto.cs index 1d07c52fd..e66591001 100644 --- a/API/DTOs/ImageDto.cs +++ b/API/DTOs/ImageDto.cs @@ -2,14 +2,14 @@ { public class ImageDto { - public int Page { get; set; } + public int Page { get; init; } public string Filename { get; init; } public string FullPath { get; init; } public int Width { get; init; } public int Height { get; init; } public string Format { get; init; } public byte[] Content { get; init; } - public string MangaFileName { get; set; } - public bool NeedsSplitting { get; set; } + public string MangaFileName { get; init; } + public bool NeedsSplitting { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index ed741642c..fb08a53e8 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -6,9 +6,9 @@ namespace API.DTOs public class LibraryDto { public int Id { get; init; } - public string Name { get; set; } - public string CoverImage { get; set; } - public LibraryType Type { get; set; } - public ICollection Folders { get; set; } + public string Name { get; init; } + public string CoverImage { get; init; } + public LibraryType Type { get; init; } + public ICollection Folders { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/LoginDto.cs b/API/DTOs/LoginDto.cs index 9983415e0..3da1841bf 100644 --- a/API/DTOs/LoginDto.cs +++ b/API/DTOs/LoginDto.cs @@ -2,7 +2,7 @@ { public class LoginDto { - public string Username { get; set; } - public string Password { get; set; } + public string Username { get; init; } + public string Password { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 26bed91b8..d7f5d5034 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -4,9 +4,9 @@ namespace API.DTOs { public class MangaFileDto { - public string FilePath { get; set; } - public int NumberOfPages { get; set; } - public MangaFormat Format { get; set; } + public string FilePath { get; init; } + public int NumberOfPages { get; init; } + public MangaFormat Format { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/MarkReadDto.cs b/API/DTOs/MarkReadDto.cs index 01f03bb83..1b39df2a8 100644 --- a/API/DTOs/MarkReadDto.cs +++ b/API/DTOs/MarkReadDto.cs @@ -2,6 +2,6 @@ { public class MarkReadDto { - public int SeriesId { get; set; } + public int SeriesId { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index b404af389..88a16aa7c 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -8,11 +8,11 @@ namespace API.DTOs /// public class MemberDto { - public int Id { get; set; } - public string Username { get; set; } - public DateTime Created { get; set; } - public DateTime LastActive { get; set; } - public IEnumerable Libraries { get; set; } - public IEnumerable Roles { get; set; } + public int Id { get; init; } + public string Username { get; init; } + public DateTime Created { get; init; } + public DateTime LastActive { get; init; } + public IEnumerable Libraries { get; init; } + public IEnumerable Roles { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index 5ff816522..b3b43bf1a 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -5,10 +5,10 @@ namespace API.DTOs public class RegisterDto { [Required] - public string Username { get; set; } + public string Username { get; init; } [Required] [StringLength(16, MinimumLength = 4)] - public string Password { get; set; } - public bool IsAdmin { get; set; } + public string Password { get; init; } + public bool IsAdmin { get; init; } } } \ No newline at end of file diff --git a/API/DTOs/SearchQueryDto.cs b/API/DTOs/SearchQueryDto.cs deleted file mode 100644 index b637f952b..000000000 --- a/API/DTOs/SearchQueryDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace API.DTOs -{ - public class SearchQueryDto - { - public string QueryString { get; init; } - } -} \ No newline at end of file diff --git a/API/DTOs/SearchResultDto.cs b/API/DTOs/SearchResultDto.cs index 3e154d3b7..114b3d03b 100644 --- a/API/DTOs/SearchResultDto.cs +++ b/API/DTOs/SearchResultDto.cs @@ -6,9 +6,7 @@ public string Name { get; init; } public string OriginalName { get; init; } public string SortName { get; init; } - public byte[] CoverImage { get; init; } // This should be optional or a thumbImage (much smaller) - - + // Grouping information public string LibraryName { get; set; } public int LibraryId { get; set; } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 7065077e4..593870309 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -8,7 +8,6 @@ public string LocalizedName { get; init; } public string SortName { get; init; } public string Summary { get; init; } - public byte[] CoverImage { get; init; } public int Pages { get; init; } /// /// Sum of pages read from linked Volumes. Calculated at API-time. diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index e9e08ec72..3fc165a6d 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -9,7 +9,6 @@ namespace API.DTOs public int Id { get; set; } public int Number { get; set; } public string Name { get; set; } - public byte[] CoverImage { get; set; } public int Pages { get; set; } public int PagesRead { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Data/LogLevelOptions.cs b/API/Data/LogLevelOptions.cs new file mode 100644 index 000000000..dfdfd111f --- /dev/null +++ b/API/Data/LogLevelOptions.cs @@ -0,0 +1,9 @@ +namespace API.Data +{ + public class LogLevelOptions + { + public const string Logging = "LogLevel"; + + public string Default { get; set; } + } +} \ No newline at end of file diff --git a/API/Data/Migrations/20210313001830_SearchIndex.Designer.cs b/API/Data/Migrations/20210313001830_SearchIndex.Designer.cs new file mode 100644 index 000000000..5aff37c5d --- /dev/null +++ b/API/Data/Migrations/20210313001830_SearchIndex.Designer.cs @@ -0,0 +1,727 @@ +// +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("20210313001830_SearchIndex")] + partial class SearchIndex + { + 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("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .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.HasIndex("Name", "NormalizedName", "LocalizedName") + .IsUnique(); + + 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/20210313001830_SearchIndex.cs b/API/Data/Migrations/20210313001830_SearchIndex.cs new file mode 100644 index 000000000..2272f73bf --- /dev/null +++ b/API/Data/Migrations/20210313001830_SearchIndex.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class SearchIndex : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName", + table: "Series", + columns: new[] { "Name", "NormalizedName", "LocalizedName" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Series_Name_NormalizedName_LocalizedName", + table: "Series"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 2d857c869..922966162 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -365,6 +365,9 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); + b.HasIndex("Name", "NormalizedName", "LocalizedName") + .IsUnique(); + b.ToTable("Series"); }); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 5e0b55d29..d1b5e3eb5 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -80,20 +80,16 @@ namespace API.Data public async Task> SearchSeries(int[] libraryIds, string searchQuery) { - var sw = Stopwatch.StartNew(); - var series = await _context.Series + return await _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") - || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")) - .Include(s => s.Library) // NOTE: Is there a way to do this faster? + || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%") + || EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) + .Include(s => s.Library) .OrderBy(s => s.SortName) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - - - _logger.LogDebug("Processed SearchSeries in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds); - return series; } public async Task> GetVolumesDtoAsync(int seriesId, int userId) @@ -109,7 +105,6 @@ namespace API.Data await AddVolumeModifiers(userId, volumes); return volumes; - } @@ -143,6 +138,16 @@ namespace API.Data .SingleOrDefaultAsync(vol => vol.Id == volumeId); } + public async Task GetVolumeDtoAsync(int volumeId) + { + return await _context.Volume + .Where(vol => vol.Id == volumeId) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) + .SingleAsync(); + + } + public async Task GetVolumeDtoAsync(int volumeId, int userId) { var volume = await _context.Volume @@ -234,6 +239,25 @@ namespace API.Data s.UserReview = rating.Review; } } + + public async Task GetVolumeCoverImageAsync(int volumeId) + { + return await _context.Volume + .Where(v => v.Id == volumeId) + .Select(v => v.CoverImage) + .AsNoTracking() + .SingleOrDefaultAsync(); + } + + public async Task GetSeriesCoverImageAsync(int seriesId) + { + return await _context.Series + .Where(s => s.Id == seriesId) + .Select(s => s.CoverImage) + .AsNoTracking() + .SingleOrDefaultAsync(); + } + private async Task AddVolumeModifiers(int userId, List volumes) { var userProgress = await _context.AppUserProgresses diff --git a/API/Data/VolumeRepository.cs b/API/Data/VolumeRepository.cs index abfb672a8..6b9e541ea 100644 --- a/API/Data/VolumeRepository.cs +++ b/API/Data/VolumeRepository.cs @@ -50,7 +50,21 @@ namespace API.Data .Where(c => c.VolumeId == volumeId) .ToListAsync(); } - + + /// + /// Returns the cover image for a chapter id. + /// + /// + /// + public async Task GetChapterCoverImageAsync(int chapterId) + { + return await _context.Chapter + .Where(c => c.Id == chapterId) + .Select(c => c.CoverImage) + .AsNoTracking() + .SingleOrDefaultAsync(); + } + public async Task GetChapterDtoAsync(int chapterId) { diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index f232d0414..c3d5ba68e 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using API.Entities.Interfaces; +using Microsoft.EntityFrameworkCore; namespace API.Entities { + [Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), IsUnique = true)] public class Series : IEntityDate { public int Id { get; set; } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 09cc03a0d..f8b10a442 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -56,5 +56,9 @@ namespace API.Extensions return services; } + + public static IServiceCollection AddStartupTask(this IServiceCollection services) + where T : class, IStartupTask + => services.AddTransient(); } } \ No newline at end of file diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 3d08cc94a..1139e0ece 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Linq; +using System.Text.Json; using API.Helpers; using Microsoft.AspNetCore.Http; @@ -18,6 +19,26 @@ namespace API.Extensions response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options)); response.Headers.Add("Access-Control-Expose-Headers", "Pagination"); } + + /// + /// Calculates SHA1 hash for a byte[] and sets as ETag. Ensures Cache-Control: private header is added. + /// + /// + /// If byte[] is null or empty, will only add cache-control + public static void AddCacheHeader(this HttpResponse response, byte[] content) + { + // Calculates SHA1 Hash for byte[] + if (content == null || content.Length <= 0) return; + using var sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider(); + response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2")))); + + // Not Needed with Response Caching + // if (!response.Headers.Keys.Contains("Cache-Control")) + // { + // response.Headers.Add("Cache-Control", "private"); + // } + + } } } \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 6a9845975..e9d950937 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -34,6 +34,12 @@ namespace API.Interfaces Task GetSeriesDtoByIdAsync(int seriesId, int userId); Task GetVolumeAsync(int volumeId); Task GetVolumeDtoAsync(int volumeId, int userId); + /// + /// A fast lookup of just the volume information with no tracking. + /// + /// + /// + Task GetVolumeDtoAsync(int volumeId); Task> GetVolumesForSeriesAsync(int[] seriesIds); Task DeleteSeriesAsync(int seriesId); Task GetVolumeByIdAsync(int volumeId); @@ -46,5 +52,8 @@ namespace API.Interfaces /// /// Task AddSeriesModifiers(int userId, List series); + + Task GetVolumeCoverImageAsync(int volumeId); + Task GetSeriesCoverImageAsync(int seriesId); } } \ No newline at end of file diff --git a/API/Interfaces/IVolumeRepository.cs b/API/Interfaces/IVolumeRepository.cs index 727133d80..faf18abb8 100644 --- a/API/Interfaces/IVolumeRepository.cs +++ b/API/Interfaces/IVolumeRepository.cs @@ -12,5 +12,6 @@ namespace API.Interfaces Task GetChapterDtoAsync(int chapterId); Task> GetFilesForChapter(int chapterId); Task> GetChaptersAsync(int volumeId); + Task GetChapterCoverImageAsync(int chapterId); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IStartupTask.cs b/API/Interfaces/Services/IStartupTask.cs new file mode 100644 index 000000000..e2a99ecad --- /dev/null +++ b/API/Interfaces/Services/IStartupTask.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace API.Interfaces.Services +{ + public interface IStartupTask + { + Task ExecuteAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 828f7cc86..1e860ab28 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -422,7 +422,7 @@ namespace API.Parser public static string Normalize(string name) { - return name.ToLower().Replace("-", "").Replace(" ", ""); + return name.ToLower().Replace("-", "").Replace(" ", "").Replace(":", ""); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 4b4f457ee..a5bdb4220 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -31,13 +31,10 @@ namespace API.Services public void EnsureCacheDirectory() { - // TODO: Replace with DirectoryService.ExistOrCreate() _logger.LogDebug("Checking if valid Cache directory: {CacheDirectory}", CacheDirectory); - var di = new DirectoryInfo(CacheDirectory); - if (!di.Exists) + if (_directoryService.ExistOrCreate(CacheDirectory)) { _logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory); - Directory.CreateDirectory(CacheDirectory); } } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 42c45d465..e1c9912be 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -39,9 +39,9 @@ namespace API.Services _cleanupService = cleanupService; _directoryService = directoryService; - + //Hangfire.RecurringJob.RemoveIfExists(); ScheduleTasks(); - //JobStorage.Current.GetMonitoringApi(). + //JobStorage.Current.GetMonitoringApi().EnqueuedJobs() } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index b09d6d544..3da2dce03 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -63,7 +63,7 @@ namespace API.Services.Tasks _scannedSeries = null; } - [DisableConcurrentExecution(timeoutInSeconds: 360)] + //[DisableConcurrentExecution(timeoutInSeconds: 360)] public void ScanLibrary(int libraryId, bool forceUpdate) { _forceUpdate = forceUpdate; diff --git a/API/Services/WarmupServiceStartupTask.cs b/API/Services/WarmupServiceStartupTask.cs new file mode 100644 index 000000000..fd9b1745b --- /dev/null +++ b/API/Services/WarmupServiceStartupTask.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using API.Interfaces.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace API.Services +{ + public class WarmupServicesStartupTask : IStartupTask + { + private readonly IServiceCollection _services; + private readonly IServiceProvider _provider; + public WarmupServicesStartupTask(IServiceCollection services, IServiceProvider provider) + { + _services = services; + _provider = provider; + } + + public Task ExecuteAsync(CancellationToken cancellationToken) + { + using (var scope = _provider.CreateScope()) + { + foreach (var singleton in GetServices(_services)) + { + scope.ServiceProvider.GetServices(singleton); + } + } + + return Task.CompletedTask; + } + + static IEnumerable GetServices(IServiceCollection services) + { + return services + .Where(descriptor => descriptor.ImplementationType != typeof(WarmupServicesStartupTask)) + .Where(descriptor => !descriptor.ServiceType.ContainsGenericParameters) + .Select(descriptor => descriptor.ServiceType) + .Distinct(); + } + } + +} \ No newline at end of file diff --git a/API/Startup.cs b/API/Startup.cs index 029fe5f5c..4e9319b37 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -1,16 +1,22 @@ +using System; using System.IO.Compression; using System.Linq; +using API.Data; using API.Extensions; using API.Middleware; +using API.Services; using Hangfire; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; namespace API @@ -40,7 +46,6 @@ namespace API { c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); }); - // This doesn't seem to work. services.AddResponseCompression(options => { options.Providers.Add(); @@ -55,6 +60,12 @@ namespace API options.Level = CompressionLevel.Fastest; }); + services.AddResponseCaching(); + + + services + .AddStartupTask() + .TryAddSingleton(services); } @@ -81,7 +92,7 @@ namespace API app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); } - //app.UseResponseCaching(); + app.UseResponseCaching(); app.UseAuthentication(); @@ -94,6 +105,19 @@ namespace API ContentTypeProvider = new FileExtensionContentTypeProvider() }); + app.Use(async (context, next) => + { + context.Response.GetTypedHeaders().CacheControl = + new Microsoft.Net.Http.Headers.CacheControlHeaderValue() + { + Public = false, + MaxAge = TimeSpan.FromSeconds(10) + }; + context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] = + new string[] { "Accept-Encoding" }; + + await next(); + }); app.UseEndpoints(endpoints => {