Random Bugs (#2531)

This commit is contained in:
Joe Milazzo 2024-01-06 10:33:56 -06:00 committed by GitHub
parent 0c70e80420
commit 4e1c66331f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 232 additions and 178 deletions

View file

@ -193,7 +193,7 @@ public class AccountController : BaseApiController
{
user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant());
}
_logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username, HttpContext.Connection.RemoteIpAddress?.ToString());
@ -390,7 +390,8 @@ public class AccountController : BaseApiController
return Ok(new InviteUserResponse
{
EmailLink = string.Empty,
EmailSent = false
EmailSent = false,
InvalidEmail = true,
});
}
@ -484,6 +485,7 @@ public class AccountController : BaseApiController
var errors = await _accountService.ValidateUsername(dto.Username);
if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "username-taken"));
user.UserName = dto.Username;
await _userManager.UpdateNormalizedUserNameAsync(user);
_unitOfWork.UserRepository.Update(user);
}
@ -689,7 +691,8 @@ public class AccountController : BaseApiController
return Ok(new InviteUserResponse
{
EmailLink = emailLink,
EmailSent = false
EmailSent = false,
InvalidEmail = true
});
}
@ -974,13 +977,12 @@ public class AccountController : BaseApiController
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
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);
_logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
if (!_emailService.IsValidEmail(user.Email))
{
_logger.LogCritical("[Email Migration]: User is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.Email);
return Ok(await _localizationService.Translate(user.Id, "invalid-email"));
_logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email);
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email"));
}
if (await _accountService.CheckIfAccessible(Request))
@ -1003,7 +1005,7 @@ public class AccountController : BaseApiController
return Ok(emailLink);
}
return Ok(await _localizationService.Translate(user.Id, "not-accessible"));
return BadRequest(await _localizationService.Translate(user.Id, "not-accessible"));
}
/// <summary>
@ -1102,12 +1104,26 @@ public class AccountController : BaseApiController
baseUrl = baseUrl.Replace("//", "/");
}
if (baseUrl.StartsWith("/"))
if (baseUrl.StartsWith('/'))
{
baseUrl = baseUrl.Substring(1, baseUrl.Length - 1);
}
}
return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey);
}
/// <summary>
/// Is the user's current email valid or not
/// </summary>
/// <returns></returns>
[HttpGet("is-email-valid")]
public async Task<ActionResult<bool>> IsEmailValid()
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
if (user == null) return Unauthorized();
if (string.IsNullOrEmpty(user.Email)) return Ok(false);
return Ok(_emailService.IsValidEmail(user.Email));
}
}

View file

@ -10,4 +10,8 @@ public class InviteUserResponse
/// Was an email sent (ie is this server accessible)
/// </summary>
public bool EmailSent { get; set; } = default!;
/// <summary>
/// When a user has an invalid email and is attempting to perform a flow.
/// </summary>
public bool InvalidEmail { get; set; } = false;
}

View file

@ -1,14 +0,0 @@
namespace API.DTOs.Account;
public class UpdateEmailResponse
{
/// <summary>
/// Did the user not have an existing email
/// </summary>
/// <remarks>This informs the user to check the new email address</remarks>
public bool HadNoExistingEmail { get; set; }
/// <summary>
/// Was an email sent (ie is this server accessible)
/// </summary>
public bool EmailSent { get; set; }
}

View file

@ -15,6 +15,7 @@ public class VolumeBuilder : IEntityBuilder<Volume>
_volume = new Volume()
{
Name = volumeNumber,
// TODO / BUG: Try to use float based Number which will allow Epub's with < 1 volumes to show in series detail
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
Chapters = new List<Chapter>()
};

View file

@ -1,4 +1,5 @@
using Serilog;
using System.Text.RegularExpressions;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting.Display;
@ -49,6 +50,7 @@ public static class LogLevelOptions
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.Enrich.With(new ApiKeyEnricher())
.WriteTo.Console(new MessageTemplateTextFormatter(outputTemplate))
.WriteTo.File(LogFile,
shared: true,
@ -74,6 +76,7 @@ public static class LogLevelOptions
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false;
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false;
}
return true;
}
@ -115,3 +118,24 @@ public static class LogLevelOptions
}
}
public partial class ApiKeyEnricher : ILogEventEnricher
{
public void Enrich(LogEvent e, ILogEventPropertyFactory propertyFactory)
{
var isRequestLoggingMiddleware = e.Properties.ContainsKey("SourceContext") &&
e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) ==
"Serilog.AspNetCore.RequestLoggingMiddleware";
if (!isRequestLoggingMiddleware) return;
if (!e.Properties.ContainsKey("RequestPath") ||
!e.Properties["RequestPath"].ToString().Contains("apiKey=")) return;
// Check if the log message contains "apiKey=" and censor it
var censoredMessage = MyRegex().Replace(e.Properties["RequestPath"].ToString(), "apiKey=******REDACTED******");
var enrichedProperty = propertyFactory.CreateProperty("RequestPath", censoredMessage);
e.AddOrUpdateProperty(enrichedProperty);
}
[GeneratedRegex(@"\bapiKey=[^&\s]+\b")]
private static partial Regex MyRegex();
}

View file

@ -1,101 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using API.Data;
using API.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace API.Middleware;
public class CustomAuthHeaderMiddleware(RequestDelegate next)
{
// Hardcoded list of allowed IP addresses in CIDR format
private readonly string[] allowedIpAddresses = { "192.168.1.0/24", "2001:db8::/32", "116.202.233.5", "104.21.81.112" };
public async Task Invoke(HttpContext context, IUnitOfWork unitOfWork, ILogger<CustomAuthHeaderMiddleware> logger, ITokenService tokenService)
{
// Extract user information from the custom header
string remoteUser = context.Request.Headers["Remote-User"];
// If header missing or user already authenticated, move on
if (string.IsNullOrEmpty(remoteUser) || context.User.Identity is {IsAuthenticated: true})
{
await next(context);
return;
}
// Validate IP address
if (IsValidIpAddress(context.Connection.RemoteIpAddress))
{
// Perform additional authentication logic if needed
// For now, you can log the authenticated user
var user = await unitOfWork.UserRepository.GetUserByEmailAsync(remoteUser);
if (user == null)
{
// Tell security log maybe?
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
// Check if the RemoteUser has an account on the server
// if (!context.Request.Path.Equals("/login", StringComparison.OrdinalIgnoreCase))
// {
// // Attach the Auth header and allow it to pass through
// var token = await tokenService.CreateToken(user);
// context.Request.Headers.Add("Authorization", $"Bearer {token}");
// //context.Response.Redirect($"/login?apiKey={user.ApiKey}");
// return;
// }
// Attach the Auth header and allow it to pass through
var token = await tokenService.CreateToken(user);
context.Request.Headers.Append("Authorization", $"Bearer {token}");
await next(context);
return;
}
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await next(context);
}
private bool IsValidIpAddress(IPAddress ipAddress)
{
// Check if the IP address is in the whitelist
return allowedIpAddresses.Any(ipRange => IpAddressRange.Parse(ipRange).Contains(ipAddress));
}
}
// Helper class for IP address range parsing
public class IpAddressRange
{
private readonly uint _startAddress;
private readonly uint _endAddress;
private IpAddressRange(uint startAddress, uint endAddress)
{
_startAddress = startAddress;
_endAddress = endAddress;
}
public bool Contains(IPAddress address)
{
var ipAddressBytes = address.GetAddressBytes();
var ipAddress = BitConverter.ToUInt32(ipAddressBytes.Reverse().ToArray(), 0);
return ipAddress >= _startAddress && ipAddress <= _endAddress;
}
public static IpAddressRange Parse(string ipRange)
{
var parts = ipRange.Split('/');
var ipAddress = IPAddress.Parse(parts[0]);
var maskBits = int.Parse(parts[1]);
var ipBytes = ipAddress.GetAddressBytes().Reverse().ToArray();
var startAddress = BitConverter.ToUInt32(ipBytes, 0);
var endAddress = startAddress | (uint.MaxValue >> maskBits);
return new IpAddressRange(startAddress, endAddress);
}
}

View file

@ -73,7 +73,7 @@ public class AccountService : IAccountService
basePart = serverSettings.HostName;
if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl))
{
var removeCount = serverSettings.BaseUrl.EndsWith("/") ? 2 : 1;
var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0;
basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - removeCount);
}
}

View file

@ -546,7 +546,6 @@ public class BookService : IBookService
ExtractSortTitle(metadataItem, epubBook, info);
}
break;
}
}

View file

@ -510,7 +510,6 @@ public class ProcessSeries : IProcessSeries
public void UpdateVolumes(Series series, IList<ParserInfo> parsedInfos, bool forceUpdate = false)
{
var startingVolumeCount = series.Volumes.Count;
// Add new volumes and update chapters per volume
var distinctVolumes = parsedInfos.DistinctVolumes();
_logger.LogDebug("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name);
@ -582,10 +581,6 @@ public class ProcessSeries : IProcessSeries
series.Volumes = nonDeletedVolumes;
}
// DO I need this anymore?
_logger.LogDebug("[ScannerService] Updated {SeriesName} volumes from count of {StartingVolumeCount} to {VolumeCount}",
series.Name, startingVolumeCount, series.Volumes.Count);
}
public void UpdateChapters(Series series, Volume volume, IList<ParserInfo> parsedInfos, bool forceUpdate = false)

View file

@ -261,7 +261,6 @@ public class Startup
app.UseMiddleware<ExceptionMiddleware>();
app.UseMiddleware<SecurityEventMiddleware>();
app.UseMiddleware<CustomAuthHeaderMiddleware>();
if (env.IsDevelopment())