Misc Polishing (#413)

* Ensure that after we assign a role to a user, we show it immediately

* Cached libraryType api as that is not going to change in a viewing session. Moved some components around to tighten bundles.

* Cleaned up more TODOs
* Refactored Configuration to use getter and setters so that the interface is a lot cleaner. Updated HashUtil to use JWT Secret instead of Machine name (as docker machine name is random each boot).
This commit is contained in:
Joseph Milazzo 2021-07-20 21:39:44 -05:00 committed by GitHub
parent ef5b22b585
commit b8165b311c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 408 additions and 307 deletions

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data;
using API.DTOs; using API.DTOs;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
@ -13,7 +12,6 @@ using Kavita.Common;
using Kavita.Common.Extensions; using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Controllers namespace API.Controllers
@ -24,26 +22,24 @@ namespace API.Controllers
private readonly ILogger<SettingsController> _logger; private readonly ILogger<SettingsController> _logger;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler; private readonly ITaskScheduler _taskScheduler;
private readonly IConfiguration _configuration;
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IConfiguration configuration) public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler)
{ {
_logger = logger; _logger = logger;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_configuration = configuration;
} }
[HttpGet("")] [HttpGet]
public async Task<ActionResult<ServerSettingDto>> GetSettings() public async Task<ActionResult<ServerSettingDto>> GetSettings()
{ {
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
settingsDto.Port = Configuration.GetPort(Program.GetAppSettingFilename()); settingsDto.Port = Configuration.Port;
settingsDto.LoggingLevel = Configuration.GetLogLevel(Program.GetAppSettingFilename()); settingsDto.LoggingLevel = Configuration.LogLevel;
return Ok(settingsDto); return Ok(settingsDto);
} }
[HttpPost("")] [HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto) public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
{ {
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
@ -61,9 +57,6 @@ namespace API.Controllers
// We do not allow CacheDirectory changes, so we will ignore. // We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
var logLevelOptions = new LogLevelOptions();
_configuration.GetSection("Logging:LogLevel").Bind(logLevelOptions);
foreach (var setting in currentSettings) foreach (var setting in currentSettings)
{ {
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
@ -78,24 +71,24 @@ namespace API.Controllers
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + "" != setting.Value) if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.Port + ""; setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json // Port is managed in appSetting.json
Configuration.UpdatePort(Program.GetAppSettingFilename(), updateSettingsDto.Port); Configuration.Port = updateSettingsDto.Port;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + "" != setting.Value) if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.LoggingLevel + ""; setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel); Configuration.LogLevel = updateSettingsDto.LoggingLevel;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + "" != setting.Value) if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.AllowStatCollection + ""; setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
if (!updateSettingsDto.AllowStatCollection) if (!updateSettingsDto.AllowStatCollection)
{ {
@ -108,7 +101,6 @@ namespace API.Controllers
} }
} }
_configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + "";
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())

View File

@ -29,7 +29,7 @@ namespace API.Data
var exists = await roleManager.RoleExistsAsync(role.Name); var exists = await roleManager.RoleExistsAsync(role.Name);
if (!exists) if (!exists)
{ {
await roleManager.CreateAsync(role); await roleManager.CreateAsync(role);
} }
} }
} }
@ -37,7 +37,7 @@ namespace API.Data
public static async Task SeedSettings(DataContext context) public static async Task SeedSettings(DataContext context)
{ {
await context.Database.EnsureCreatedAsync(); await context.Database.EnsureCreatedAsync();
IList<ServerSetting> defaultSettings = new List<ServerSetting>() IList<ServerSetting> defaultSettings = new List<ServerSetting>()
{ {
new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory}, new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory},
@ -46,7 +46,7 @@ namespace API.Data
new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"}, new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"},
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))}, new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))},
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
}; };
foreach (var defaultSetting in defaultSettings) foreach (var defaultSetting in defaultSettings)
@ -61,14 +61,13 @@ namespace API.Data
await context.SaveChangesAsync(); await context.SaveChangesAsync();
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match // Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
var configFile = Program.GetAppSettingFilename();
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.Port).Value = context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.Port).Value =
Configuration.GetPort(configFile) + ""; Configuration.Port + string.Empty;
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value = context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value =
Configuration.GetLogLevel(configFile); Configuration.LogLevel + string.Empty;
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }
} }

View File

@ -43,7 +43,7 @@ namespace API.Extensions
services.AddDbContext<DataContext>(options => services.AddDbContext<DataContext>(options =>
{ {
options.UseSqlite(config.GetConnectionString("DefaultConnection")); options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug")); options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.LogLevel.Equals("Debug"));
}); });
} }

View File

@ -18,132 +18,117 @@ using Sentry;
namespace API namespace API
{ {
public class Program public class Program
{ {
private static int _httpPort; private static readonly int HttpPort = Configuration.Port;
protected Program() protected Program()
{ {
} }
public static string GetAppSettingFilename() public static async Task Main(string[] args)
{ {
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); Console.OutputEncoding = System.Text.Encoding.UTF8;
var isDevelopment = environment == Environments.Development;
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json";
}
public static async Task Main(string[] args) // Before anything, check if JWT has been generated properly or if user still has default
{ if (!Configuration.CheckIfJwtTokenSet() &&
Console.OutputEncoding = System.Text.Encoding.UTF8; Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development)
{
Console.WriteLine("Generating JWT TokenKey for encrypting user sessions...");
var rBytes = new byte[128];
using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes);
Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty);
}
// Before anything, check if JWT has been generated properly or if user still has default var host = CreateHostBuilder(args).Build();
if (!Configuration.CheckIfJwtTokenSet(GetAppSettingFilename()) && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development)
using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<DataContext>();
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
// Apply all migrations on startup
await context.Database.MigrateAsync();
await Seed.SeedRoles(roleManager);
await Seed.SeedSettings(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred during migration");
}
await host.RunAsync();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{ {
Console.WriteLine("Generating JWT TokenKey for encrypting user sessions..."); webBuilder.UseKestrel((opts) =>
var rBytes = new byte[128]; {
using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes); opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
var base64 = Convert.ToBase64String(rBytes).Replace("/", ""); });
Configuration.UpdateJwtToken(GetAppSettingFilename(), base64);
}
// Get HttpPort from Config var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
_httpPort = Configuration.GetPort(GetAppSettingFilename()); if (environment != Environments.Development)
{
webBuilder.UseSentry(options =>
{
options.Dsn = "https://40f4e7b49c094172a6f99d61efb2740f@o641015.ingest.sentry.io/5757423";
options.MaxBreadcrumbs = 200;
options.AttachStacktrace = true;
options.Debug = false;
options.SendDefaultPii = false;
options.DiagnosticLevel = SentryLevel.Debug;
options.ShutdownTimeout = TimeSpan.FromSeconds(5);
options.Release = BuildInfo.Version.ToString();
options.AddExceptionFilterForType<OutOfMemoryException>();
options.AddExceptionFilterForType<NetVips.VipsException>();
options.AddExceptionFilterForType<InvalidDataException>();
options.AddExceptionFilterForType<KavitaException>();
options.BeforeSend = sentryEvent =>
var host = CreateHostBuilder(args).Build(); {
if (sentryEvent.Exception != null
using var scope = host.Services.CreateScope(); && sentryEvent.Exception.Message.StartsWith("[GetCoverImage]")
var services = scope.ServiceProvider; && sentryEvent.Exception.Message.StartsWith("[BookService]")
&& sentryEvent.Exception.Message.StartsWith("[ExtractArchive]")
try && sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
{ && sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
var context = services.GetRequiredService<DataContext>(); && sentryEvent.Exception.Message.StartsWith("[GetNumberOfPagesFromArchive]")
var roleManager = services.GetRequiredService<RoleManager<AppRole>>(); && sentryEvent.Exception.Message.Contains("EPUB parsing error")
// Apply all migrations on startup && sentryEvent.Exception.Message.Contains("Unsupported EPUB version")
await context.Database.MigrateAsync(); && sentryEvent.Exception.Message.Contains("Incorrect EPUB")
await Seed.SeedRoles(roleManager); && sentryEvent.Exception.Message.Contains("Access is Denied"))
await Seed.SeedSettings(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService <ILogger<Program>>();
logger.LogError(ex, "An error occurred during migration");
}
await host.RunAsync();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel((opts) =>
{
opts.ListenAnyIP(_httpPort, options =>
{ {
options.Protocols = HttpProtocols.Http1AndHttp2; return null; // Don't send this event to Sentry
}); }
});
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); sentryEvent.ServerName = null; // Never send Server Name to Sentry
if (environment != Environments.Development) return sentryEvent;
{ };
webBuilder.UseSentry(options =>
options.ConfigureScope(scope =>
{
scope.User = new User()
{ {
options.Dsn = "https://40f4e7b49c094172a6f99d61efb2740f@o641015.ingest.sentry.io/5757423"; Id = HashUtil.AnonymousToken()
options.MaxBreadcrumbs = 200; };
options.AttachStacktrace = true; scope.Contexts.App.Name = BuildInfo.AppName;
options.Debug = false; scope.Contexts.App.Version = BuildInfo.Version.ToString();
options.SendDefaultPii = false; scope.Contexts.App.StartTime = DateTime.UtcNow;
options.DiagnosticLevel = SentryLevel.Debug; scope.Contexts.App.Hash = HashUtil.AnonymousToken();
options.ShutdownTimeout = TimeSpan.FromSeconds(5); scope.Contexts.App.Build = BuildInfo.Release;
options.Release = BuildInfo.Version.ToString(); scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
options.AddExceptionFilterForType<OutOfMemoryException>(); scope.SetTag("branch", BuildInfo.Branch);
options.AddExceptionFilterForType<NetVips.VipsException>(); });
options.AddExceptionFilterForType<InvalidDataException>(); });
options.AddExceptionFilterForType<KavitaException>(); }
options.BeforeSend = sentryEvent => webBuilder.UseStartup<Startup>();
{ });
if (sentryEvent.Exception != null }
&& sentryEvent.Exception.Message.StartsWith("[GetCoverImage]")
&& sentryEvent.Exception.Message.StartsWith("[BookService]")
&& sentryEvent.Exception.Message.StartsWith("[ExtractArchive]")
&& sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
&& sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
&& sentryEvent.Exception.Message.StartsWith("[GetNumberOfPagesFromArchive]")
&& sentryEvent.Exception.Message.Contains("EPUB parsing error")
&& sentryEvent.Exception.Message.Contains("Unsupported EPUB version")
&& sentryEvent.Exception.Message.Contains("Incorrect EPUB")
&& sentryEvent.Exception.Message.Contains("Access is Denied"))
{
return null; // Don't send this event to Sentry
}
sentryEvent.ServerName = null; // Never send Server Name to Sentry
return sentryEvent;
};
options.ConfigureScope(scope =>
{
scope.User = new User()
{
Id = HashUtil.AnonymousToken()
};
scope.Contexts.App.Name = BuildInfo.AppName;
scope.Contexts.App.Version = BuildInfo.Version.ToString();
scope.Contexts.App.StartTime = DateTime.UtcNow;
scope.Contexts.App.Hash = HashUtil.AnonymousToken();
scope.Contexts.App.Build = BuildInfo.Release;
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
scope.SetTag("branch", BuildInfo.Branch);
});
});
}
webBuilder.UseStartup<Startup>();
});
}
} }

View File

@ -146,7 +146,7 @@ namespace API
}); });
} }
private void OnShutdown() private static void OnShutdown()
{ {
Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs..."); Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
TaskScheduler.Client.Dispose(); TaskScheduler.Client.Dispose();

View File

@ -2,133 +2,259 @@
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using Microsoft.Extensions.Hosting;
namespace Kavita.Common namespace Kavita.Common
{ {
public static class Configuration public static class Configuration
{ {
#region JWT Token private static string AppSettingsFilename = GetAppSettingFilename();
public static bool CheckIfJwtTokenSet(string filePath) public static string Branch
{ {
try { get => GetBranch(GetAppSettingFilename());
var json = File.ReadAllText(filePath); set => SetBranch(GetAppSettingFilename(), value);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json); }
const string key = "TokenKey";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetString() != "super secret unguessable key";
}
return false; public static int Port
{
} get => GetPort(GetAppSettingFilename());
catch (Exception ex) { set => SetPort(GetAppSettingFilename(), value);
Console.WriteLine("Error writing app settings: " + ex.Message); }
public static string JwtToken
{
get => GetJwtToken(GetAppSettingFilename());
set => SetJwtToken(GetAppSettingFilename(), value);
}
public static string LogLevel
{
get => GetLogLevel(GetAppSettingFilename());
set => SetLogLevel(GetAppSettingFilename(), value);
}
private static string GetAppSettingFilename()
{
if (!string.IsNullOrEmpty(AppSettingsFilename))
{
return AppSettingsFilename;
}
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var isDevelopment = environment == Environments.Development;
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json";
}
#region JWT Token
private static string GetJwtToken(string filePath)
{
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "TokenKey";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetString();
} }
return string.Empty;
}
catch (Exception ex)
{
Console.WriteLine("Error reading app settings: " + ex.Message);
}
return string.Empty;
}
private static bool SetJwtToken(string filePath, string token)
{
try
{
var currentToken = GetJwtToken(filePath);
var json = File.ReadAllText(filePath)
.Replace("\"TokenKey\": \"" + currentToken, "\"TokenKey\": \"" + token);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false; return false;
} }
public static bool UpdateJwtToken(string filePath, string token) }
{
try
{
var json = File.ReadAllText(filePath).Replace("super secret unguessable key", token);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
#endregion
#region Port
public static bool UpdatePort(string filePath, int port)
{
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{
return true;
}
try
{
var currentPort = GetPort(filePath);
var json = File.ReadAllText(filePath).Replace("\"Port\": " + currentPort, "\"Port\": " + port);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
public static int GetPort(string filePath)
{
const int defaultPort = 5000;
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{
return defaultPort;
}
try {
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "Port";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetInt32();
}
}
catch (Exception ex) {
Console.WriteLine("Error writing app settings: " + ex.Message);
}
public static bool CheckIfJwtTokenSet()
{
//string filePath
try
{
return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key";
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return false;
}
public static bool UpdateJwtToken(string token)
{
try
{
var filePath = GetAppSettingFilename();
var json = File.ReadAllText(filePath).Replace("super secret unguessable key", token);
File.WriteAllText(GetAppSettingFilename(), json);
return true;
}
catch (Exception)
{
return false;
}
}
#endregion
#region Port
public static bool SetPort(string filePath, int port)
{
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{
return true;
}
try
{
var currentPort = GetPort(filePath);
var json = File.ReadAllText(filePath).Replace("\"Port\": " + currentPort, "\"Port\": " + port);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
public static int GetPort(string filePath)
{
Console.WriteLine(GetAppSettingFilename());
const int defaultPort = 5000;
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{
return defaultPort; return defaultPort;
} }
#endregion
#region LogLevel
public static bool UpdateLogLevel(string filePath, string logLevel)
{
try
{
var currentLevel = GetLogLevel(filePath);
var json = File.ReadAllText(filePath).Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\"");
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
public static string GetLogLevel(string filePath)
{
try {
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement))
{
foreach (var property in tokenElement.EnumerateObject())
{
if (!property.Name.Equals("LogLevel")) continue;
foreach (var logProperty in property.Value.EnumerateObject())
{
if (logProperty.Name.Equals("Default"))
{
return logProperty.Value.GetString();
}
}
}
}
}
catch (Exception ex) {
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return "Information"; try
} {
#endregion var json = File.ReadAllText(filePath);
} var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
} const string key = "Port";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetInt32();
}
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return defaultPort;
}
#endregion
#region LogLevel
public static bool SetLogLevel(string filePath, string logLevel)
{
try
{
var currentLevel = GetLogLevel(filePath);
var json = File.ReadAllText(filePath)
.Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\"");
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
public static string GetLogLevel(string filePath)
{
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement))
{
foreach (var property in tokenElement.EnumerateObject())
{
if (!property.Name.Equals("LogLevel")) continue;
foreach (var logProperty in property.Value.EnumerateObject())
{
if (logProperty.Name.Equals("Default"))
{
return logProperty.Value.GetString();
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return "Information";
}
#endregion
public static string GetBranch(string filePath)
{
const string defaultBranch = "main";
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "Branch";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetString();
}
}
catch (Exception ex)
{
Console.WriteLine("Error reading app settings: " + ex.Message);
}
return defaultBranch;
}
public static bool SetBranch(string filePath, string updatedBranch)
{
try
{
var currentBranch = GetBranch(filePath);
var json = File.ReadAllText(filePath)
.Replace("\"Branch\": " + currentBranch, "\"Branch\": " + updatedBranch);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@ -34,7 +34,7 @@ namespace Kavita.Common
/// <returns></returns> /// <returns></returns>
public static string AnonymousToken() public static string AnonymousToken()
{ {
var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Environment.MachineName}_{Environment.UserName}"; var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Configuration.JwtToken}_{Environment.UserName}";
return CalculateCrc(seed); return CalculateCrc(seed);
} }
} }

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.7.0" /> <PackageReference Include="Sentry" Version="3.7.0" />
</ItemGroup> </ItemGroup>

View File

@ -160,7 +160,6 @@
<div [ngbNavOutlet]="nav" class="mt-3"></div> <div [ngbNavOutlet]="nav" class="mt-3"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<!-- TODO: Replace secondary buttons in modals with btn-light -->
<button type="button" class="btn btn-secondary" (click)="close()">Close</button> <button type="button" class="btn btn-secondary" (click)="close()">Close</button>
<button type="submit" class="btn btn-primary" (click)="save()">Save</button> <button type="submit" class="btn btn-primary" (click)="save()">Save</button>
</div> </div>

View File

@ -1,4 +1,3 @@
//TODO: Refactor this name to something better
export interface InProgressChapter { export interface InProgressChapter {
id: number; id: number;
range: string; range: string;

View File

@ -14,7 +14,8 @@ export class LibraryService {
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
libraryNames: {[key:number]: string} | undefined = undefined; private libraryNames: {[key:number]: string} | undefined = undefined;
private libraryTypes: {[key: number]: LibraryType} | undefined = undefined;
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
@ -75,8 +76,17 @@ export class LibraryService {
} }
getLibraryType(libraryId: number) { getLibraryType(libraryId: number) {
// TODO: Cache this in browser if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) {
return this.httpClient.get<LibraryType>(this.baseUrl + 'library/type?libraryId=' + libraryId); return of(this.libraryTypes[libraryId]);
}
return this.httpClient.get<LibraryType>(this.baseUrl + 'library/type?libraryId=' + libraryId).pipe(map(l => {
if (this.libraryTypes === undefined) {
this.libraryTypes = {};
}
this.libraryTypes[libraryId] = l;
return this.libraryTypes[libraryId];
}));
} }
search(term: string) { search(term: string) {

View File

@ -42,7 +42,6 @@ export class DirectoryPickerComponent implements OnInit {
} }
goBack() { goBack() {
// BUG: When Going back to initial listing, this code gets stuck on first drive
this.routeStack.pop(); this.routeStack.pop();
const stackPeek = this.routeStack.peek(); const stackPeek = this.routeStack.peek();
if (stackPeek !== undefined) { if (stackPeek !== undefined) {
@ -53,7 +52,6 @@ export class DirectoryPickerComponent implements OnInit {
this.currentRoot = ''; this.currentRoot = '';
this.loadChildren(this.currentRoot); this.loadChildren(this.currentRoot);
} }
} }
loadChildren(path: string) { loadChildren(path: string) {

View File

@ -30,7 +30,7 @@ export class EditRbsModalComponent implements OnInit {
} }
close() { close() {
this.modal.close(false); this.modal.close(undefined);
} }
save() { save() {
@ -42,8 +42,10 @@ export class EditRbsModalComponent implements OnInit {
this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => { this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => {
if (this.member) { if (this.member) {
this.member.roles = selectedRoles; this.member.roles = selectedRoles;
this.modal.close(this.member);
return;
} }
this.modal.close(true); this.modal.close(undefined);
}); });
} }

View File

@ -69,7 +69,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => { this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => {
this.deletionInProgress = false; this.deletionInProgress = false;
this.getLibraries(); this.getLibraries();
this.toastr.success('Library has been removed'); // BUG: This is not causing a refresh this.toastr.success('Library has been removed');
}); });
} }
} }

View File

@ -25,18 +25,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="stat-collection">Allow Anonymous Usage Collection</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="statTooltip" role="button" tabindex="0"></i> <label for="stat-collection" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<ng-template #statTooltip>Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect.</ng-template> <p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
<span class="sr-only" id="logging-level-port-help">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect.</span>
<p class="accent">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect</p>
<div class="form-check"> <div class="form-check">
<input id="stat-collection" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="allowStatCollection"> <input id="stat-collection" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="allowStatCollection">
<label for="stat-collection" class="form-check-label">Send Data</label> <label for="stat-collection" class="form-check-label">Send Data</label>
</div> </div>
</div> </div>
<h4>Reoccuring Tasks</h4> <h4>Reoccuring Tasks</h4>
<div class="form-group"> <div class="form-group">
<label for="settings-tasks-scan">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i> <label for="settings-tasks-scan">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>

View File

@ -82,6 +82,11 @@ export class ManageUsersComponent implements OnInit {
openEditRole(member: Member) { openEditRole(member: Member) {
const modalRef = this.modalService.open(EditRbsModalComponent); const modalRef = this.modalService.open(EditRbsModalComponent);
modalRef.componentInstance.member = member; modalRef.componentInstance.member = member;
modalRef.closed.subscribe((updatedMember: Member) => {
if (updatedMember !== undefined) {
member = updatedMember;
}
})
} }
updatePassword(member: Member) { updatePassword(member: Member) {

View File

@ -37,6 +37,8 @@ import { TypeaheadModule } from './typeahead/typeahead.module';
import { AllCollectionsComponent } from './all-collections/all-collections.component'; import { AllCollectionsComponent } from './all-collections/all-collections.component';
import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component'; import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component';
import { RecentlyAddedComponent } from './recently-added/recently-added.component'; import { RecentlyAddedComponent } from './recently-added/recently-added.component';
import { LibraryCardComponent } from './library-card/library-card.component';
import { SeriesCardComponent } from './series-card/series-card.component';
let sentryProviders: any[] = []; let sentryProviders: any[] = [];
@ -100,6 +102,8 @@ if (environment.production) {
AllCollectionsComponent, AllCollectionsComponent,
EditCollectionTagsComponent, EditCollectionTagsComponent,
RecentlyAddedComponent, RecentlyAddedComponent,
LibraryCardComponent,
SeriesCardComponent
], ],
imports: [ imports: [
HttpClientModule, HttpClientModule,

View File

@ -27,7 +27,7 @@
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading"> <div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading">
<app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages" [urlProvider]="getPageUrl"></app-infinite-scroller> <app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages" [urlProvider]="getPageUrl"></app-infinite-scroller>
</div> </div>
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <!--; else webtoonClickArea; TODO: See if people want this mode WEBTOON_WITH_CLICKS--> <ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <!--; else webtoonClickArea; See if people want this mode WEBTOON_WITH_CLICKS-->
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'top'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'top'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div>
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div>
</ng-container> </ng-container>

View File

@ -7,10 +7,9 @@ import { EditSeriesModalComponent } from 'src/app/_modals/edit-series-modal/edit
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service'; import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { ConfirmService } from '../confirm.service'; import { ConfirmService } from '../shared/confirm.service';
@Component({ @Component({
selector: 'app-series-card', selector: 'app-series-card',
@ -30,9 +29,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
constructor(private accountService: AccountService, private router: Router, constructor(private accountService: AccountService, private router: Router,
private seriesService: SeriesService, private toastr: ToastrService, private seriesService: SeriesService, private toastr: ToastrService,
private libraryService: LibraryService, private modalService: NgbModal, private modalService: NgbModal, private confirmService: ConfirmService,
private confirmService: ConfirmService, public imageService: ImageService, public imageService: ImageService, private actionFactoryService: ActionFactoryService) {
private actionFactoryService: ActionFactoryService) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) { if (user) {
this.isAdmin = this.accountService.hasAdminRole(user); this.isAdmin = this.accountService.hasAdminRole(user);

View File

@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { CardItemComponent } from './card-item/card-item.component'; import { CardItemComponent } from './card-item/card-item.component';
import { NgbCollapseModule, NgbDropdownModule, NgbPaginationModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapseModule, NgbDropdownModule, NgbPaginationModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { LibraryCardComponent } from './library-card/library-card.component';
import { SeriesCardComponent } from './series-card/series-card.component';
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component'; import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { SafeHtmlPipe } from './safe-html.pipe'; import { SafeHtmlPipe } from './safe-html.pipe';
import { LazyLoadImageModule } from 'ng-lazyload-image'; import { LazyLoadImageModule } from 'ng-lazyload-image';
import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component'; import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component';
import { RegisterMemberComponent } from './register-member/register-member.component'; import { RegisterMemberComponent } from '../register-member/register-member.component';
import { ReadMoreComponent } from './read-more/read-more.component'; import { ReadMoreComponent } from './read-more/read-more.component';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DrawerComponent } from './drawer/drawer.component'; import { DrawerComponent } from './drawer/drawer.component';
@ -24,8 +22,6 @@ import { A11yClickDirective } from './a11y-click.directive';
declarations: [ declarations: [
RegisterMemberComponent, RegisterMemberComponent,
CardItemComponent, CardItemComponent,
LibraryCardComponent,
SeriesCardComponent,
CardDetailsModalComponent, CardDetailsModalComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
SafeHtmlPipe, SafeHtmlPipe,
@ -49,10 +45,8 @@ import { A11yClickDirective } from './a11y-click.directive';
NgbPaginationModule // CardDetailLayoutComponent NgbPaginationModule // CardDetailLayoutComponent
], ],
exports: [ exports: [
RegisterMemberComponent, // TODO: Move this out and put in normal app RegisterMemberComponent,
CardItemComponent, CardItemComponent,
LibraryCardComponent, // TODO: Move this out and put in normal app
SeriesCardComponent, // TODO: Move this out and put in normal app
SafeHtmlPipe, SafeHtmlPipe,
CardActionablesComponent, CardActionablesComponent,
ReadMoreComponent, ReadMoreComponent,

View File

@ -27,13 +27,6 @@ export class SelectionModel<T> {
}); });
} }
// __lookupItem(item: T) {
// if (this._propAccessor != '') {
// // TODO: Implement this code to speedup lookups (use a map rather than array)
// }
// const dataItem = this._data.filter(data => data.value == d);
// }
/** /**
* Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true. * Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true.
* @param data Item to toggle * @param data Item to toggle