Logging Enhancements (#1521)
* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch. * Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating. * Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation. * Fixed up the backup service to not rely on Config from appsettings.json * Tweaked the Logging levels available * Moved everything over to File-scoped namespaces * Moved everything over to File-scoped namespaces * Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data * Removed dead code
This commit is contained in:
parent
9f715cc35f
commit
d1a14f7e68
212 changed files with 16599 additions and 16834 deletions
|
|
@ -15,74 +15,71 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class ApplicationServiceExtensions
|
||||
{
|
||||
public static class ApplicationServiceExtensions
|
||||
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
|
||||
{
|
||||
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
|
||||
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
||||
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<IDirectoryService, DirectoryService>();
|
||||
services.AddScoped<ITokenService, TokenService>();
|
||||
services.AddScoped<IFileSystem, FileSystem>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.AddScoped<ICacheHelper, CacheHelper>();
|
||||
|
||||
services.AddScoped<IStatsService, StatsService>();
|
||||
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
||||
services.AddScoped<ICacheService, CacheService>();
|
||||
services.AddScoped<IArchiveService, ArchiveService>();
|
||||
services.AddScoped<IBackupService, BackupService>();
|
||||
services.AddScoped<ICleanupService, CleanupService>();
|
||||
services.AddScoped<IBookService, BookService>();
|
||||
services.AddScoped<IImageService, ImageService>();
|
||||
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
||||
services.AddScoped<IDownloadService, DownloadService>();
|
||||
services.AddScoped<IReaderService, ReaderService>();
|
||||
services.AddScoped<IReadingItemService, ReadingItemService>();
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IEmailService, EmailService>();
|
||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||
services.AddScoped<IThemeService, ThemeService>();
|
||||
services.AddScoped<ISeriesService, SeriesService>();
|
||||
services.AddScoped<IProcessSeries, ProcessSeries>();
|
||||
services.AddScoped<IReadingListService, ReadingListService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IMetadataService, MetadataService>();
|
||||
services.AddScoped<IWordCountAnalyzerService, WordCountAnalyzerService>();
|
||||
services.AddScoped<ILibraryWatcher, LibraryWatcher>();
|
||||
|
||||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||
services.AddScoped<IEventHub, EventHub>();
|
||||
|
||||
services.AddSqLite(config, env);
|
||||
services.AddLogging(config);
|
||||
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
||||
}
|
||||
|
||||
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
||||
IHostEnvironment env)
|
||||
{
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
options.EnableDetailedErrors();
|
||||
options.EnableSensitiveDataLogging(env.IsDevelopment());
|
||||
});
|
||||
}
|
||||
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<IDirectoryService, DirectoryService>();
|
||||
services.AddScoped<ITokenService, TokenService>();
|
||||
services.AddScoped<IFileSystem, FileSystem>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.AddScoped<ICacheHelper, CacheHelper>();
|
||||
|
||||
services.AddScoped<IStatsService, StatsService>();
|
||||
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
||||
services.AddScoped<ICacheService, CacheService>();
|
||||
services.AddScoped<IArchiveService, ArchiveService>();
|
||||
services.AddScoped<IBackupService, BackupService>();
|
||||
services.AddScoped<ICleanupService, CleanupService>();
|
||||
services.AddScoped<IBookService, BookService>();
|
||||
services.AddScoped<IImageService, ImageService>();
|
||||
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
||||
services.AddScoped<IDownloadService, DownloadService>();
|
||||
services.AddScoped<IReaderService, ReaderService>();
|
||||
services.AddScoped<IReadingItemService, ReadingItemService>();
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IEmailService, EmailService>();
|
||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||
services.AddScoped<IThemeService, ThemeService>();
|
||||
services.AddScoped<ISeriesService, SeriesService>();
|
||||
services.AddScoped<IProcessSeries, ProcessSeries>();
|
||||
services.AddScoped<IReadingListService, ReadingListService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IMetadataService, MetadataService>();
|
||||
services.AddScoped<IWordCountAnalyzerService, WordCountAnalyzerService>();
|
||||
services.AddScoped<ILibraryWatcher, LibraryWatcher>();
|
||||
|
||||
|
||||
|
||||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||
services.AddScoped<IEventHub, EventHub>();
|
||||
|
||||
services.AddSqLite(config, env);
|
||||
services.AddLogging(config);
|
||||
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
||||
}
|
||||
|
||||
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
||||
IHostEnvironment env)
|
||||
private static void AddLogging(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddLogging(loggingBuilder =>
|
||||
{
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
options.EnableDetailedErrors();
|
||||
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.LogLevel.Equals("Debug"));
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddLogging(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddLogging(loggingBuilder =>
|
||||
{
|
||||
var loggingSection = config.GetSection("Logging");
|
||||
loggingBuilder.AddFile(loggingSection);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,33 +3,32 @@ using System.Linq;
|
|||
using API.Entities;
|
||||
using API.Parser;
|
||||
|
||||
namespace API.Extensions
|
||||
{
|
||||
public static class ChapterListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns first chapter in the list with at least one file
|
||||
/// </summary>
|
||||
/// <param name="chapters"></param>
|
||||
/// <returns></returns>
|
||||
public static Chapter GetFirstChapterWithFiles(this IList<Chapter> chapters)
|
||||
{
|
||||
return chapters.FirstOrDefault(c => c.Files.Any());
|
||||
}
|
||||
namespace API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
|
||||
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
|
||||
/// </summary>
|
||||
/// <param name="chapters"></param>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
public static Chapter GetChapterByRange(this IList<Chapter> chapters, ParserInfo info)
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
return specialTreatment
|
||||
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
|
||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
||||
}
|
||||
public static class ChapterListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns first chapter in the list with at least one file
|
||||
/// </summary>
|
||||
/// <param name="chapters"></param>
|
||||
/// <returns></returns>
|
||||
public static Chapter GetFirstChapterWithFiles(this IList<Chapter> chapters)
|
||||
{
|
||||
return chapters.FirstOrDefault(c => c.Files.Any());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
|
||||
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
|
||||
/// </summary>
|
||||
/// <param name="chapters"></param>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
public static Chapter GetChapterByRange(this IList<Chapter> chapters, ParserInfo info)
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
return specialTreatment
|
||||
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
|
||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
using System.Security.Claims;
|
||||
using Kavita.Common;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
public static class ClaimsPrincipalExtensions
|
||||
public static string GetUsername(this ClaimsPrincipal user)
|
||||
{
|
||||
public static string GetUsername(this ClaimsPrincipal user)
|
||||
{
|
||||
var userClaim = user.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userClaim == null) throw new KavitaException("User is not authenticated");
|
||||
return userClaim.Value;
|
||||
}
|
||||
var userClaim = user.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userClaim == null) throw new KavitaException("User is not authenticated");
|
||||
return userClaim.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static class ConfigurationExtensions
|
||||
public static int GetMaxRollingFiles(this IConfiguration config)
|
||||
{
|
||||
public static int GetMaxRollingFiles(this IConfiguration config)
|
||||
{
|
||||
return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value);
|
||||
}
|
||||
public static string GetLoggingFileName(this IConfiguration config)
|
||||
{
|
||||
return config.GetSection("Logging").GetSection("File").GetSection("Path").Value;
|
||||
}
|
||||
return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value);
|
||||
}
|
||||
}
|
||||
public static string GetLoggingFileName(this IConfiguration config)
|
||||
{
|
||||
return config.GetSection("Logging").GetSection("File").GetSection("Path").Value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,28 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500));
|
||||
|
||||
/// <summary>
|
||||
/// A natural sort implementation
|
||||
/// </summary>
|
||||
/// <param name="items">IEnumerable to process</param>
|
||||
/// <param name="selector">Function that produces a string. Does not support null values</param>
|
||||
/// <param name="stringComparer">Defaults to CurrentCulture</param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>Sorted Enumerable</returns>
|
||||
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
|
||||
{
|
||||
private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500));
|
||||
var list = items.ToList();
|
||||
var maxDigits = list
|
||||
.SelectMany(i => Regex.Matches(selector(i))
|
||||
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
||||
.Max() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// A natural sort implementation
|
||||
/// </summary>
|
||||
/// <param name="items">IEnumerable to process</param>
|
||||
/// <param name="selector">Function that produces a string. Does not support null values</param>
|
||||
/// <param name="stringComparer">Defaults to CurrentCulture</param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>Sorted Enumerable</returns>
|
||||
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
|
||||
{
|
||||
var list = items.ToList();
|
||||
var maxDigits = list
|
||||
.SelectMany(i => Regex.Matches(selector(i))
|
||||
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
||||
.Max() ?? 0;
|
||||
|
||||
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
||||
}
|
||||
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class FileInfoExtensions
|
||||
{
|
||||
public static class FileInfoExtensions
|
||||
/// <summary>
|
||||
/// Checks if the last write time of the file is after the passed date
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="comparison"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the last write time of the file is after the passed date
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="comparison"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
|
||||
{
|
||||
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
|
||||
}
|
||||
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,19 @@ using System.Collections.Generic;
|
|||
using API.DTOs.Filtering;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class FilterDtoExtensions
|
||||
{
|
||||
public static class FilterDtoExtensions
|
||||
private static readonly IList<MangaFormat> AllFormats = Enum.GetValues<MangaFormat>();
|
||||
|
||||
public static IList<MangaFormat> GetSqlFilter(this FilterDto filter)
|
||||
{
|
||||
private static readonly IList<MangaFormat> AllFormats = Enum.GetValues<MangaFormat>();
|
||||
|
||||
public static IList<MangaFormat> GetSqlFilter(this FilterDto filter)
|
||||
if (filter.Formats == null || filter.Formats.Count == 0)
|
||||
{
|
||||
if (filter.Formats == null || filter.Formats.Count == 0)
|
||||
{
|
||||
return AllFormats;
|
||||
}
|
||||
|
||||
return filter.Formats;
|
||||
return AllFormats;
|
||||
}
|
||||
|
||||
return filter.Formats;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,53 +9,52 @@ using API.Helpers;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class HttpExtensions
|
||||
{
|
||||
public static class HttpExtensions
|
||||
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
|
||||
int itemsPerPage, int totalItems, int totalPages)
|
||||
{
|
||||
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
|
||||
int itemsPerPage, int totalItems, int totalPages)
|
||||
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
|
||||
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
|
||||
}
|
||||
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
|
||||
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates SHA256 hash for a byte[] and sets as ETag. Ensures Cache-Control: private header is added.
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="content">If byte[] is null or empty, will only add cache-control</param>
|
||||
public static void AddCacheHeader(this HttpResponse response, byte[] content)
|
||||
/// <summary>
|
||||
/// Calculates SHA256 hash for a byte[] and sets as ETag. Ensures Cache-Control: private header is added.
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="content">If byte[] is null or empty, will only add cache-control</param>
|
||||
public static void AddCacheHeader(this HttpResponse response, byte[] content)
|
||||
{
|
||||
if (content is not {Length: > 0}) return;
|
||||
using var sha1 = SHA256.Create();
|
||||
|
||||
response.Headers.Add(HeaderNames.ETag, string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2"))));
|
||||
response.Headers.CacheControl = $"private,max-age=100";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates SHA256 hash for a cover image filename and sets as ETag. Ensures Cache-Control: private header is added.
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="filename"></param>
|
||||
/// <param name="maxAge">Maximum amount of seconds to set for Cache-Control</param>
|
||||
public static void AddCacheHeader(this HttpResponse response, string filename, int maxAge = 10)
|
||||
{
|
||||
if (filename is not {Length: > 0}) return;
|
||||
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
|
||||
using var sha1 = SHA256.Create();
|
||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
|
||||
if (maxAge != 10)
|
||||
{
|
||||
if (content is not {Length: > 0}) return;
|
||||
using var sha1 = SHA256.Create();
|
||||
|
||||
response.Headers.Add(HeaderNames.ETag, string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2"))));
|
||||
response.Headers.CacheControl = $"private,max-age=100";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates SHA256 hash for a cover image filename and sets as ETag. Ensures Cache-Control: private header is added.
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="filename"></param>
|
||||
/// <param name="maxAge">Maximum amount of seconds to set for Cache-Control</param>
|
||||
public static void AddCacheHeader(this HttpResponse response, string filename, int maxAge = 10)
|
||||
{
|
||||
if (filename is not {Length: > 0}) return;
|
||||
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
|
||||
using var sha1 = SHA256.Create();
|
||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
|
||||
if (maxAge != 10)
|
||||
{
|
||||
response.Headers.CacheControl = $"max-age={maxAge}";
|
||||
}
|
||||
response.Headers.CacheControl = $"max-age={maxAge}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,79 +10,78 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class IdentityServiceExtensions
|
||||
{
|
||||
public static class IdentityServiceExtensions
|
||||
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
options.User.AllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/";
|
||||
});
|
||||
|
||||
services.AddIdentityCore<AppUser>(opt =>
|
||||
{
|
||||
options.User.AllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/";
|
||||
});
|
||||
opt.Password.RequireNonAlphanumeric = false;
|
||||
opt.Password.RequireDigit = false;
|
||||
opt.Password.RequireDigit = false;
|
||||
opt.Password.RequireLowercase = false;
|
||||
opt.Password.RequireUppercase = false;
|
||||
opt.Password.RequireNonAlphanumeric = false;
|
||||
opt.Password.RequiredLength = 6;
|
||||
|
||||
services.AddIdentityCore<AppUser>(opt =>
|
||||
opt.SignIn.RequireConfirmedEmail = true;
|
||||
|
||||
opt.Lockout.AllowedForNewUsers = true;
|
||||
opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
|
||||
opt.Lockout.MaxFailedAccessAttempts = 5;
|
||||
|
||||
})
|
||||
.AddTokenProvider<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
|
||||
.AddRoles<AppRole>()
|
||||
.AddRoleManager<RoleManager<AppRole>>()
|
||||
.AddSignInManager<SignInManager<AppUser>>()
|
||||
.AddRoleValidator<RoleValidator<AppRole>>()
|
||||
.AddEntityFrameworkStores<DataContext>();
|
||||
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters()
|
||||
{
|
||||
opt.Password.RequireNonAlphanumeric = false;
|
||||
opt.Password.RequireDigit = false;
|
||||
opt.Password.RequireDigit = false;
|
||||
opt.Password.RequireLowercase = false;
|
||||
opt.Password.RequireUppercase = false;
|
||||
opt.Password.RequireNonAlphanumeric = false;
|
||||
opt.Password.RequiredLength = 6;
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidIssuer = "Kavita"
|
||||
};
|
||||
|
||||
opt.SignIn.RequireConfirmedEmail = true;
|
||||
|
||||
opt.Lockout.AllowedForNewUsers = true;
|
||||
opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
|
||||
opt.Lockout.MaxFailedAccessAttempts = 5;
|
||||
|
||||
})
|
||||
.AddTokenProvider<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
|
||||
.AddRoles<AppRole>()
|
||||
.AddRoleManager<RoleManager<AppRole>>()
|
||||
.AddSignInManager<SignInManager<AppUser>>()
|
||||
.AddRoleValidator<RoleValidator<AppRole>>()
|
||||
.AddEntityFrameworkStores<DataContext>();
|
||||
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
options.Events = new JwtBearerEvents()
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters()
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidIssuer = "Kavita"
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents()
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
// Only use query string based token on SignalR hubs
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
// Only use query string based token on SignalR hubs
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
context.Token = accessToken;
|
||||
}
|
||||
};
|
||||
});
|
||||
services.AddAuthorization(opt =>
|
||||
{
|
||||
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
services.AddAuthorization(opt =>
|
||||
{
|
||||
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,30 @@ using System.Linq;
|
|||
using API.Entities;
|
||||
using API.Parser;
|
||||
|
||||
namespace API.Extensions
|
||||
{
|
||||
public static class ParserInfoListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects distinct volume numbers by the "Volumes" key on the ParserInfo
|
||||
/// </summary>
|
||||
/// <param name="infos"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<string> DistinctVolumes(this IList<ParserInfo> infos)
|
||||
{
|
||||
return infos.Select(p => p.Volumes).Distinct().ToList();
|
||||
}
|
||||
namespace API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a list of ParserInfos has a given chapter or not. Lookup occurs on Range property. If a chapter is
|
||||
/// special, then the <see cref="ParserInfo.Filename"/> is matched, else the <see cref="ParserInfo.Chapters"/> field is checked.
|
||||
/// </summary>
|
||||
/// <param name="infos"></param>
|
||||
/// <param name="chapter"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
|
||||
{
|
||||
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
|
||||
: infos.Any(v => v.Chapters == chapter.Range);
|
||||
}
|
||||
public static class ParserInfoListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects distinct volume numbers by the "Volumes" key on the ParserInfo
|
||||
/// </summary>
|
||||
/// <param name="infos"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<string> DistinctVolumes(this IList<ParserInfo> infos)
|
||||
{
|
||||
return infos.Select(p => p.Volumes).Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a list of ParserInfos has a given chapter or not. Lookup occurs on Range property. If a chapter is
|
||||
/// special, then the <see cref="ParserInfo.Filename"/> is matched, else the <see cref="ParserInfo.Chapters"/> field is checked.
|
||||
/// </summary>
|
||||
/// <param name="infos"></param>
|
||||
/// <param name="chapter"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
|
||||
{
|
||||
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
|
||||
: infos.Any(v => v.Chapters == chapter.Range);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,46 +4,45 @@ using API.Entities;
|
|||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class SeriesExtensions
|
||||
{
|
||||
public static class SeriesExtensions
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format.
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="list"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInList(this Series series, IEnumerable<string> list)
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format.
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="list"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInList(this Series series, IEnumerable<string> list)
|
||||
{
|
||||
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| name == series.Name || name == series.LocalizedName || name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName));
|
||||
}
|
||||
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| name == series.Name || name == series.LocalizedName || name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches anything in the list. Includes a check against the Format of the Series
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="list"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInList(this Series series, IEnumerable<ParsedSeries> list)
|
||||
{
|
||||
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| name.Name == series.Name || name.Name == series.LocalizedName || name.Name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName) && series.Format == name.Format);
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches anything in the list. Includes a check against the Format of the Series
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="list"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInList(this Series series, IEnumerable<ParsedSeries> list)
|
||||
{
|
||||
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| name.Name == series.Name || name.Name == series.LocalizedName || name.Name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName) && series.Format == name.Format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches the <see cref="ParserInfo"/>
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInParserInfo(this Series series, ParserInfo info)
|
||||
{
|
||||
if (info == null) return false;
|
||||
return Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName
|
||||
|| Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName);
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks against all the name variables of the Series if it matches the <see cref="ParserInfo"/>
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
public static bool NameInParserInfo(this Series series, ParserInfo info)
|
||||
{
|
||||
if (info == null) return false;
|
||||
return Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|
||||
|| info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName
|
||||
|| Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,28 @@ using API.Comparators;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Extensions
|
||||
{
|
||||
public static class VolumeListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
|
||||
/// If there are both specials and non-specials, then the first non-special will be returned.
|
||||
/// </summary>
|
||||
/// <param name="volumes"></param>
|
||||
/// <param name="seriesFormat"></param>
|
||||
/// <returns></returns>
|
||||
public static Volume GetCoverImage(this IList<Volume> volumes, MangaFormat seriesFormat)
|
||||
{
|
||||
if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf)
|
||||
{
|
||||
return volumes.OrderBy(x => x.Number).FirstOrDefault();
|
||||
}
|
||||
namespace API.Extensions;
|
||||
|
||||
if (volumes.Any(x => x.Number != 0))
|
||||
{
|
||||
return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
|
||||
}
|
||||
public static class VolumeListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
|
||||
/// If there are both specials and non-specials, then the first non-special will be returned.
|
||||
/// </summary>
|
||||
/// <param name="volumes"></param>
|
||||
/// <param name="seriesFormat"></param>
|
||||
/// <returns></returns>
|
||||
public static Volume GetCoverImage(this IList<Volume> volumes, MangaFormat seriesFormat)
|
||||
{
|
||||
if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf)
|
||||
{
|
||||
return volumes.OrderBy(x => x.Number).FirstOrDefault();
|
||||
}
|
||||
|
||||
if (volumes.Any(x => x.Number != 0))
|
||||
{
|
||||
return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
|
||||
}
|
||||
return volumes.OrderBy(x => x.Number).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,17 @@
|
|||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
|
||||
namespace API.Extensions
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class ZipArchiveExtensions
|
||||
{
|
||||
public static class ZipArchiveExtensions
|
||||
/// <summary>
|
||||
/// Checks if archive has one or more files. Excludes directory entries.
|
||||
/// </summary>
|
||||
/// <param name="archive"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasFiles(this ZipArchive archive)
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if archive has one or more files. Excludes directory entries.
|
||||
/// </summary>
|
||||
/// <param name="archive"></param>
|
||||
/// <returns></returns>
|
||||
public static bool HasFiles(this ZipArchive archive)
|
||||
{
|
||||
return archive.Entries.Any(x => Path.HasExtension(x.FullName));
|
||||
}
|
||||
return archive.Entries.Any(x => Path.HasExtension(x.FullName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue