Holiday Bugfixes (#1762)
* Don't show "not much going on" when we are actively downloading * Swipe to paginate is now behind a flag in the user preferences. * Added a new server setting for host name, if the server sits behind a reverse proxy. If this is set, email link generation will use it and will not perform any checks on accessibility (thus email will always send) * Refactored the code that checks if the server is accessible to check if host name is set, and thus return rue if so. * Added back the system drawing library for markdown parsing. * Fixed a validation error * Fixed a bug where folder watching could get re-triggered when it was disabled at a server level. * Made the manga reader loader absolute positioned for better visibility * Indentation
This commit is contained in:
parent
2a47029209
commit
5e9bbd0768
31 changed files with 1986 additions and 49 deletions
|
@ -96,6 +96,7 @@
|
|||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||
|
|
|
@ -324,10 +324,11 @@ public class AccountController : BaseApiController
|
|||
// Send a confirmation email
|
||||
try
|
||||
{
|
||||
var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||
_logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
var accessible = await _emailService.CheckIfAccessible(host);
|
||||
|
||||
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
try
|
||||
|
@ -495,7 +496,7 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||
|
||||
return GenerateEmailLink(user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
||||
}
|
||||
|
||||
|
||||
|
@ -603,11 +604,10 @@ public class AccountController : BaseApiController
|
|||
|
||||
try
|
||||
{
|
||||
var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email", dto.Email);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
|
||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
var accessible = await _emailService.CheckIfAccessible(host);
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
try
|
||||
|
@ -795,10 +795,9 @@ public class AccountController : BaseApiController
|
|||
return BadRequest("You do not have an email on account or it has not been confirmed");
|
||||
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
if (await _emailService.CheckIfAccessible(host))
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
{
|
||||
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
||||
{
|
||||
|
@ -851,6 +850,11 @@ public class AccountController : BaseApiController
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resend an invite to a user already invited
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("resend-confirmation-email")]
|
||||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
|
@ -863,26 +867,22 @@ public class AccountController : BaseApiController
|
|||
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var emailLink = GenerateEmailLink(token, "confirm-email", user.Email);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
||||
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
||||
_logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token);
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
Username = user.UserName,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
Username = user.UserName,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
return Ok(emailLink);
|
||||
}
|
||||
|
||||
|
||||
return Ok(emailLink);
|
||||
}
|
||||
|
||||
private string GenerateEmailLink(string token, string routePart, string email, bool withHost = true)
|
||||
{
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
if (withHost) return $"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||
return Ok("The server is not accessible externally. Please ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -36,10 +36,11 @@ public class ServerController : BaseApiController
|
|||
private readonly IEmailService _emailService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService)
|
||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
|
@ -51,6 +52,7 @@ public class ServerController : BaseApiController
|
|||
_emailService = emailService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -189,12 +191,13 @@ public class ServerController : BaseApiController
|
|||
/// <summary>
|
||||
/// Is this server accessible to the outside net
|
||||
/// </summary>
|
||||
/// <remarks>If the instance has the HostName set, this will return true whether or not it is accessible externally</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("accessible")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||
{
|
||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
||||
return Ok(await _accountService.CheckIfAccessible(Request));
|
||||
}
|
||||
|
||||
[HttpGet("jobs")]
|
||||
|
|
|
@ -104,7 +104,7 @@ public class SettingsController : BaseApiController
|
|||
[HttpPost]
|
||||
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());
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
|
@ -182,6 +182,13 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = (updateSettingsDto.HostName + string.Empty).Trim();
|
||||
if (setting.Value.EndsWith("/")) setting.Value = setting.Value.Substring(0, setting.Value.Length - 1);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
|
|
|
@ -111,6 +111,7 @@ public class UsersController : BaseApiController
|
|||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
|
|
|
@ -66,4 +66,8 @@ public class ServerSettingDto
|
|||
/// If the server should save covers as WebP encoding
|
||||
/// </summary>
|
||||
public bool ConvertCoverToWebP { get; set; }
|
||||
/// <summary>
|
||||
/// The Host name (ie Reverse proxy domain name) for the server
|
||||
/// </summary>
|
||||
public string HostName { get; set; }
|
||||
}
|
||||
|
|
|
@ -46,6 +46,11 @@ public class UserPreferencesDto
|
|||
/// </summary>
|
||||
[Required]
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
[Required]
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </summary>
|
||||
|
|
1746
API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs
generated
Normal file
1746
API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
26
API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs
Normal file
26
API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class SwipeToPaginatePref : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SwipeToPaginate",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SwipeToPaginate",
|
||||
table: "AppUserPreferences");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -249,6 +249,9 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("ShowScreenHints")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SwipeToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ThemeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ public static class Seed
|
|||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using API.Entities.Enums;
|
||||
using System;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
|
||||
namespace API.Entities;
|
||||
|
@ -26,7 +27,6 @@ public class AppUserPreferences
|
|||
/// </example>
|
||||
/// </summary>
|
||||
public ReaderMode ReaderMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </summary>
|
||||
|
@ -48,6 +48,10 @@ public class AppUserPreferences
|
|||
/// </summary>
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override extra Margin
|
||||
/// </summary>
|
||||
public int BookReaderMargin { get; set; } = 15;
|
||||
|
|
|
@ -105,4 +105,10 @@ public enum ServerSettingKey
|
|||
/// </summary>
|
||||
[Description("ConvertCoverToWebP")]
|
||||
ConvertCoverToWebP = 19,
|
||||
/// <summary>
|
||||
/// The Host name (ie Reverse proxy domain name) for the server. Used for email link generation
|
||||
/// </summary>
|
||||
[Description("HostName")]
|
||||
HostName = 20,
|
||||
|
||||
}
|
||||
|
|
|
@ -66,6 +66,9 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
|||
case ServerSettingKey.TotalLogs:
|
||||
destination.TotalLogs = int.Parse(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.HostName:
|
||||
destination.HostName = row.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Errors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -20,6 +23,8 @@ public interface IAccountService
|
|||
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
||||
Task<bool> HasBookmarkPermission(AppUser user);
|
||||
Task<bool> HasDownloadPermission(AppUser user);
|
||||
Task<bool> CheckIfAccessible(HttpRequest request);
|
||||
Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true);
|
||||
}
|
||||
|
||||
public class AccountService : IAccountService
|
||||
|
@ -27,13 +32,44 @@ public class AccountService : IAccountService
|
|||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ILogger<AccountService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IEmailService _emailService;
|
||||
public const string DefaultPassword = "[k.2@RZ!mxCQkJzE";
|
||||
private const string LocalHost = "localhost:4200";
|
||||
|
||||
public AccountService(UserManager<AppUser> userManager, ILogger<AccountService> logger, IUnitOfWork unitOfWork)
|
||||
public AccountService(UserManager<AppUser> userManager, ILogger<AccountService> logger, IUnitOfWork unitOfWork,
|
||||
IHostEnvironment environment, IEmailService emailService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_environment = environment;
|
||||
_emailService = emailService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> CheckIfAccessible(HttpRequest request)
|
||||
{
|
||||
var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString();
|
||||
return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true)
|
||||
{
|
||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString();
|
||||
var basePart = $"{request.Scheme}://{host}{request.PathBase}/";
|
||||
if (!string.IsNullOrEmpty(serverSettings.HostName))
|
||||
{
|
||||
basePart = serverSettings.HostName;
|
||||
}
|
||||
|
||||
if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)
|
||||
|
|
|
@ -74,7 +74,14 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
|
||||
public async Task StartWatching()
|
||||
{
|
||||
_logger.LogInformation("[LibraryWatcher] Starting file watchers");
|
||||
FileWatchers.Clear();
|
||||
WatcherDictionary.Clear();
|
||||
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching)
|
||||
{
|
||||
_logger.LogInformation("Folder watching is disabled at the server level, thus ignoring any requests to create folder watching");
|
||||
return;
|
||||
}
|
||||
|
||||
var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
.Where(l => l.FolderWatching)
|
||||
|
@ -84,6 +91,8 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
.Where(_directoryService.Exists)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation("[LibraryWatcher] Starting file watchers for {Count} library folders", libraryFolders.Count);
|
||||
|
||||
foreach (var libraryFolder in libraryFolders)
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
||||
|
@ -107,7 +116,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||
|
||||
WatcherDictionary[libraryFolder].Add(watcher);
|
||||
}
|
||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", FileWatchers.Count);
|
||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", libraryFolders.Count);
|
||||
}
|
||||
|
||||
public void StopWatching()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue