mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
ef5b22b585
commit
b8165b311c
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
@ -13,7 +12,6 @@ using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
@ -24,26 +22,24 @@ namespace API.Controllers
|
||||
private readonly ILogger<SettingsController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
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;
|
||||
_unitOfWork = unitOfWork;
|
||||
_taskScheduler = taskScheduler;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
settingsDto.Port = Configuration.GetPort(Program.GetAppSettingFilename());
|
||||
settingsDto.LoggingLevel = Configuration.GetLogLevel(Program.GetAppSettingFilename());
|
||||
settingsDto.Port = Configuration.Port;
|
||||
settingsDto.LoggingLevel = Configuration.LogLevel;
|
||||
return Ok(settingsDto);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
_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.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
|
||||
var logLevelOptions = new LogLevelOptions();
|
||||
_configuration.GetSection("Logging:LogLevel").Bind(logLevelOptions);
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
@ -78,24 +71,24 @@ namespace API.Controllers
|
||||
_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
|
||||
Configuration.UpdatePort(Program.GetAppSettingFilename(), updateSettingsDto.Port);
|
||||
Configuration.Port = updateSettingsDto.Port;
|
||||
_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 + "";
|
||||
Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel);
|
||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||
Configuration.LogLevel = updateSettingsDto.LoggingLevel;
|
||||
_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);
|
||||
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() || !await _unitOfWork.CommitAsync())
|
||||
|
@ -61,11 +61,10 @@ namespace API.Data
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// 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 =
|
||||
Configuration.GetPort(configFile) + "";
|
||||
Configuration.Port + string.Empty;
|
||||
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
||||
Configuration.GetLogLevel(configFile);
|
||||
Configuration.LogLevel + string.Empty;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
@ -43,7 +43,7 @@ namespace API.Extensions
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug"));
|
||||
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.LogLevel.Equals("Debug"));
|
||||
});
|
||||
}
|
||||
|
||||
|
219
API/Program.cs
219
API/Program.cs
@ -18,132 +18,117 @@ using Sentry;
|
||||
|
||||
namespace API
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private static int _httpPort;
|
||||
public class Program
|
||||
{
|
||||
private static readonly int HttpPort = Configuration.Port;
|
||||
|
||||
protected Program()
|
||||
{
|
||||
}
|
||||
protected Program()
|
||||
{
|
||||
}
|
||||
|
||||
public static string GetAppSettingFilename()
|
||||
{
|
||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
var isDevelopment = environment == Environments.Development;
|
||||
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json";
|
||||
}
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
// Before anything, check if JWT has been generated properly or if user still has default
|
||||
if (!Configuration.CheckIfJwtTokenSet() &&
|
||||
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
|
||||
if (!Configuration.CheckIfJwtTokenSet(GetAppSettingFilename()) && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development)
|
||||
var host = CreateHostBuilder(args).Build();
|
||||
|
||||
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...");
|
||||
var rBytes = new byte[128];
|
||||
using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes);
|
||||
var base64 = Convert.ToBase64String(rBytes).Replace("/", "");
|
||||
Configuration.UpdateJwtToken(GetAppSettingFilename(), base64);
|
||||
}
|
||||
webBuilder.UseKestrel((opts) =>
|
||||
{
|
||||
opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
|
||||
});
|
||||
|
||||
// Get HttpPort from Config
|
||||
_httpPort = Configuration.GetPort(GetAppSettingFilename());
|
||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
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>();
|
||||
|
||||
|
||||
var host = CreateHostBuilder(args).Build();
|
||||
|
||||
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 =>
|
||||
{
|
||||
webBuilder.UseKestrel((opts) =>
|
||||
{
|
||||
opts.ListenAnyIP(_httpPort, options =>
|
||||
options.BeforeSend = sentryEvent =>
|
||||
{
|
||||
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"))
|
||||
{
|
||||
options.Protocols = HttpProtocols.Http1AndHttp2;
|
||||
});
|
||||
});
|
||||
return null; // Don't send this event to Sentry
|
||||
}
|
||||
|
||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
if (environment != Environments.Development)
|
||||
{
|
||||
webBuilder.UseSentry(options =>
|
||||
sentryEvent.ServerName = null; // Never send Server Name to Sentry
|
||||
return sentryEvent;
|
||||
};
|
||||
|
||||
options.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new User()
|
||||
{
|
||||
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>();
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
options.BeforeSend = sentryEvent =>
|
||||
{
|
||||
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>();
|
||||
});
|
||||
}
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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...");
|
||||
TaskScheduler.Client.Dispose();
|
||||
|
@ -2,133 +2,259 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Kavita.Common
|
||||
{
|
||||
public static class Configuration
|
||||
{
|
||||
#region JWT Token
|
||||
public static bool CheckIfJwtTokenSet(string filePath)
|
||||
{
|
||||
try {
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||
const string key = "TokenKey";
|
||||
public static class Configuration
|
||||
{
|
||||
private static string AppSettingsFilename = GetAppSettingFilename();
|
||||
public static string Branch
|
||||
{
|
||||
get => GetBranch(GetAppSettingFilename());
|
||||
set => SetBranch(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||
{
|
||||
return tokenElement.GetString() != "super secret unguessable key";
|
||||
}
|
||||
public static int Port
|
||||
{
|
||||
get => GetPort(GetAppSettingFilename());
|
||||
set => SetPort(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
return false;
|
||||
public static string JwtToken
|
||||
{
|
||||
get => GetJwtToken(GetAppSettingFilename());
|
||||
set => SetJwtToken(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Console.WriteLine("Error writing app settings: " + ex.Message);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||
const string key = "Port";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||
{
|
||||
return tokenElement.GetInt32();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Console.WriteLine("Error writing app settings: " + ex.Message);
|
||||
}
|
||||
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;
|
||||
}
|
||||
#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";
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ namespace Kavita.Common
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -160,7 +160,6 @@
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
</div>
|
||||
<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="submit" class="btn btn-primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
//TODO: Refactor this name to something better
|
||||
export interface InProgressChapter {
|
||||
id: number;
|
||||
range: string;
|
||||
|
@ -14,7 +14,8 @@ export class LibraryService {
|
||||
|
||||
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) {}
|
||||
|
||||
@ -75,8 +76,17 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
getLibraryType(libraryId: number) {
|
||||
// TODO: Cache this in browser
|
||||
return this.httpClient.get<LibraryType>(this.baseUrl + 'library/type?libraryId=' + libraryId);
|
||||
if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(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) {
|
||||
|
@ -42,7 +42,6 @@ export class DirectoryPickerComponent implements OnInit {
|
||||
}
|
||||
|
||||
goBack() {
|
||||
// BUG: When Going back to initial listing, this code gets stuck on first drive
|
||||
this.routeStack.pop();
|
||||
const stackPeek = this.routeStack.peek();
|
||||
if (stackPeek !== undefined) {
|
||||
@ -53,7 +52,6 @@ export class DirectoryPickerComponent implements OnInit {
|
||||
this.currentRoot = '';
|
||||
this.loadChildren(this.currentRoot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadChildren(path: string) {
|
||||
|
@ -30,7 +30,7 @@ export class EditRbsModalComponent implements OnInit {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close(false);
|
||||
this.modal.close(undefined);
|
||||
}
|
||||
|
||||
save() {
|
||||
@ -42,8 +42,10 @@ export class EditRbsModalComponent implements OnInit {
|
||||
this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => {
|
||||
if (this.member) {
|
||||
this.member.roles = selectedRoles;
|
||||
this.modal.close(this.member);
|
||||
return;
|
||||
}
|
||||
this.modal.close(true);
|
||||
this.modal.close(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => {
|
||||
this.deletionInProgress = false;
|
||||
this.getLibraries();
|
||||
this.toastr.success('Library has been removed'); // BUG: This is not causing a refresh
|
||||
this.toastr.success('Library has been removed');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,14 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="stat-collection">Allow Anonymous Usage Collection</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="statTooltip" role="button" tabindex="0"></i>
|
||||
<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>
|
||||
<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>
|
||||
<label for="stat-collection" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
||||
<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>
|
||||
<div class="form-check">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h4>Reoccuring Tasks</h4>
|
||||
<div class="form-group">
|
||||
<label for="settings-tasks-scan">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||
|
@ -82,6 +82,11 @@ export class ManageUsersComponent implements OnInit {
|
||||
openEditRole(member: Member) {
|
||||
const modalRef = this.modalService.open(EditRbsModalComponent);
|
||||
modalRef.componentInstance.member = member;
|
||||
modalRef.closed.subscribe((updatedMember: Member) => {
|
||||
if (updatedMember !== undefined) {
|
||||
member = updatedMember;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updatePassword(member: Member) {
|
||||
|
@ -37,6 +37,8 @@ import { TypeaheadModule } from './typeahead/typeahead.module';
|
||||
import { AllCollectionsComponent } from './all-collections/all-collections.component';
|
||||
import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.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[] = [];
|
||||
|
||||
@ -100,6 +102,8 @@ if (environment.production) {
|
||||
AllCollectionsComponent,
|
||||
EditCollectionTagsComponent,
|
||||
RecentlyAddedComponent,
|
||||
LibraryCardComponent,
|
||||
SeriesCardComponent
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
|
@ -27,7 +27,7 @@
|
||||
<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>
|
||||
</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 ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div>
|
||||
</ng-container>
|
||||
|
@ -7,10 +7,9 @@ import { EditSeriesModalComponent } from 'src/app/_modals/edit-series-modal/edit
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { AccountService } from 'src/app/_services/account.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 { SeriesService } from 'src/app/_services/series.service';
|
||||
import { ConfirmService } from '../confirm.service';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-card',
|
||||
@ -30,9 +29,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private libraryService: LibraryService, private modalService: NgbModal,
|
||||
private confirmService: ConfirmService, public imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService) {
|
||||
private modalService: NgbModal, private confirmService: ConfirmService,
|
||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { CardItemComponent } from './card-item/card-item.component';
|
||||
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 { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
||||
import { SafeHtmlPipe } from './safe-html.pipe';
|
||||
import { LazyLoadImageModule } from 'ng-lazyload-image';
|
||||
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 { RouterModule } from '@angular/router';
|
||||
import { DrawerComponent } from './drawer/drawer.component';
|
||||
@ -24,8 +22,6 @@ import { A11yClickDirective } from './a11y-click.directive';
|
||||
declarations: [
|
||||
RegisterMemberComponent,
|
||||
CardItemComponent,
|
||||
LibraryCardComponent,
|
||||
SeriesCardComponent,
|
||||
CardDetailsModalComponent,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
@ -49,10 +45,8 @@ import { A11yClickDirective } from './a11y-click.directive';
|
||||
NgbPaginationModule // CardDetailLayoutComponent
|
||||
],
|
||||
exports: [
|
||||
RegisterMemberComponent, // TODO: Move this out and put in normal app
|
||||
RegisterMemberComponent,
|
||||
CardItemComponent,
|
||||
LibraryCardComponent, // TODO: Move this out and put in normal app
|
||||
SeriesCardComponent, // TODO: Move this out and put in normal app
|
||||
SafeHtmlPipe,
|
||||
CardActionablesComponent,
|
||||
ReadMoreComponent,
|
||||
|
@ -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.
|
||||
* @param data Item to toggle
|
||||
|
Loading…
x
Reference in New Issue
Block a user