From 1f80afc75994dde1abde106d6c30809e6448203d Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sat, 7 Sep 2024 14:13:51 +0200 Subject: [PATCH] Polish codes in UI project. --- .../Attributes/CodeColumnAttribute.cs | 4 +- .../Interfaces/IAuthorizationFilters.cs | 37 ++-- .../AuthorizationFilterService.cs | 67 +++--- .../BasicAuthServiceByConfiguration.cs | 31 +++ .../Filters/BasicAuthenticationFilter.cs | 10 +- .../Filters/IBasicAuthenticationService.cs | 48 +---- .../LocalRequestsOnlyAuthorizationFilter.cs | 19 +- .../Filters/PolicyAuthorizationFilter.cs | 15 +- .../IAuthorizationFilterService.cs | 19 +- .../Endpoints/AppStreamLoader.cs | 20 +- .../Endpoints/IAppStreamLoader.cs | 16 +- .../Endpoints/ISerilogUiAppRoutes.cs | 27 ++- .../Endpoints/ISerilogUiEndpoints.cs | 24 ++- .../Endpoints/ISerilogUiOptionsSetter.cs | 21 +- .../Endpoints/JsonSerializerOptionsFactory.cs | 10 + .../Endpoints/SerilogUiAppRoutes.cs | 131 +++++------- .../Endpoints/SerilogUiAppRoutesDecorator.cs | 72 +++---- .../Endpoints/SerilogUiEndpoints.cs | 200 ++++++++---------- .../Endpoints/SerilogUiEndpointsDecorator.cs | 38 ++-- .../ApplicationBuilderExtensions.cs | 45 ++-- .../Extensions/HttpRequestExtensions.cs | 37 ++-- .../SerilogUiOptionBuilderExtensions.cs | 1 - .../Extensions/ServiceCollectionExtensions.cs | 72 +++---- src/Serilog.Ui.Web/GlobalUsings.cs | 14 ++ .../Models/AuthenticationType.cs | 8 + .../Models/AuthorizationOptions.cs | 40 ++-- src/Serilog.Ui.Web/Models/UiOptions.cs | 9 +- src/Serilog.Ui.Web/SerilogUiMiddleware.cs | 151 ++++++------- 28 files changed, 585 insertions(+), 601 deletions(-) create mode 100644 src/Serilog.Ui.Web/Authorization/Filters/BasicAuthServiceByConfiguration.cs create mode 100644 src/Serilog.Ui.Web/Endpoints/JsonSerializerOptionsFactory.cs create mode 100644 src/Serilog.Ui.Web/GlobalUsings.cs create mode 100644 src/Serilog.Ui.Web/Models/AuthenticationType.cs diff --git a/src/Serilog.Ui.Core/Attributes/CodeColumnAttribute.cs b/src/Serilog.Ui.Core/Attributes/CodeColumnAttribute.cs index 424591c5..94556fed 100644 --- a/src/Serilog.Ui.Core/Attributes/CodeColumnAttribute.cs +++ b/src/Serilog.Ui.Core/Attributes/CodeColumnAttribute.cs @@ -12,5 +12,5 @@ public class CodeColumnAttribute(CodeType codeType) : Attribute /// /// Gets the CodeType. /// - public readonly CodeType CodeType = codeType; -} + public CodeType CodeType { get; } = codeType; +} \ No newline at end of file diff --git a/src/Serilog.Ui.Core/Interfaces/IAuthorizationFilters.cs b/src/Serilog.Ui.Core/Interfaces/IAuthorizationFilters.cs index d3069f3b..ce427a56 100644 --- a/src/Serilog.Ui.Core/Interfaces/IAuthorizationFilters.cs +++ b/src/Serilog.Ui.Core/Interfaces/IAuthorizationFilters.cs @@ -1,28 +1,27 @@ using System.Threading.Tasks; -namespace Serilog.Ui.Core.Interfaces +namespace Serilog.Ui.Core.Interfaces; + +/// +/// Authorization filter, used to authorize access to Serilog Ui pages. +/// Runs synchronous. +/// +public interface IUiAuthorizationFilter { /// - /// Authorization filter, used to authorize access to Serilog Ui pages. - /// Runs synchronous. + /// Authorizes a request sync. /// - public interface IUiAuthorizationFilter - { - /// - /// Authorizes a request sync. - /// - bool Authorize(); - } + bool Authorize(); +} +/// +/// Authorization filter, used to authorize access to Serilog Ui pages. +/// Runs asynchronous. +/// +public interface IUiAsyncAuthorizationFilter +{ /// - /// Authorization filter, used to authorize access to Serilog Ui pages. - /// Runs asynchronous. + /// Authorizes a request async. /// - public interface IUiAsyncAuthorizationFilter - { - /// - /// Authorizes a request async. - /// - Task AuthorizeAsync(); - } + Task AuthorizeAsync(); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/AuthorizationFilterService.cs b/src/Serilog.Ui.Web/Authorization/AuthorizationFilterService.cs index 6cba6742..1c86e128 100644 --- a/src/Serilog.Ui.Web/Authorization/AuthorizationFilterService.cs +++ b/src/Serilog.Ui.Web/Authorization/AuthorizationFilterService.cs @@ -1,48 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Serilog.Ui.Core.Interfaces; +using Serilog.Ui.Core.Interfaces; -namespace Serilog.Ui.Web.Authorization +namespace Serilog.Ui.Web.Authorization; + +internal sealed class AuthorizationFilterService( + IHttpContextAccessor httpContextAccessor, + IEnumerable syncFilters, + IEnumerable asyncFilters + ) : IAuthorizationFilterService { - internal sealed class AuthorizationFilterService( - IHttpContextAccessor httpContextAccessor, - IEnumerable syncFilters, - IEnumerable asyncFilters) : IAuthorizationFilterService + private readonly HttpContext _httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); + + public async Task CheckAccessAsync(Func onSuccess, Func? onFailure = null) { - public async Task CheckAccessAsync( - Func onSuccess, - Func? onFailure = null) + bool accessCheck = await CanAccessAsync(); + if (accessCheck) { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext is null) return; - - var accessCheck = await CanAccessAsync(); - - if (accessCheck) - { - await onSuccess(); - return; - } - - httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - if (onFailure != null) - { - await onFailure.Invoke(); - } + await onSuccess(); + return; } - private async Task CanAccessAsync() + _httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + if (onFailure != null) { - var syncFilterResult = syncFilters.Any(filter => !filter.Authorize()); + await onFailure.Invoke(); + } + } - var asyncFilter = await Task.WhenAll(asyncFilters.Select(filter => filter.AuthorizeAsync())); - var asyncFilterResult = Array.Exists(asyncFilter, filter => !filter); + private async Task CanAccessAsync() + { + // Evaluate all synchronous filters and check if any of them deny access. + bool syncAuthorizeResult = syncFilters.Any(filter => !filter.Authorize()); - return !syncFilterResult && !asyncFilterResult; - } + // Evaluate all asynchronous filters and check if any of them deny access. + bool[] asyncFilter = await Task.WhenAll(asyncFilters.Select(filter => filter.AuthorizeAsync())); + bool asyncAuthorizeResult = Array.Exists(asyncFilter, filter => !filter); + + // Return true if all filters grant access, otherwise return false. + return !syncAuthorizeResult && !asyncAuthorizeResult; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthServiceByConfiguration.cs b/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthServiceByConfiguration.cs new file mode 100644 index 00000000..b8b019e9 --- /dev/null +++ b/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthServiceByConfiguration.cs @@ -0,0 +1,31 @@ +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.Configuration; + +namespace Serilog.Ui.Web.Authorization.Filters; + +internal class BasicAuthServiceByConfiguration(IConfiguration configuration) : IBasicAuthenticationService +{ + private readonly string? _userName = configuration["SerilogUi:UserName"]; + private readonly string? _password = configuration["SerilogUi:Password"]; + + public Task CanAccessAsync(AuthenticationHeaderValue basicHeader) + { + var header = basicHeader.Parameter; + + return Task.FromResult(EvaluateAuthResult(header)); + } + + private bool EvaluateAuthResult(string? header) + { + var (userName, password) = ExtractAuthenticationTokens(header); + return userName == _userName && password == _password; + } + + private static (string userName, string password) ExtractAuthenticationTokens(string? authValues) + { + var parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authValues ?? string.Empty)); + var parts = parameter.Split(':'); + return (parts[0], parts[1]); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthenticationFilter.cs b/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthenticationFilter.cs index 7bd6246b..4b89c5f9 100644 --- a/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthenticationFilter.cs +++ b/src/Serilog.Ui.Web/Authorization/Filters/BasicAuthenticationFilter.cs @@ -1,7 +1,4 @@ -using System; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Net.Http.Headers; using Serilog.Ui.Core.Interfaces; namespace Serilog.Ui.Web.Authorization.Filters; @@ -16,7 +13,10 @@ internal class BasicAuthenticationFilter( public async Task AuthorizeAsync() { var httpContext = httpContextAccessor.HttpContext; - if (httpContext is null) return false; + if (httpContext is null) + { + return false; + } var header = httpContext.Request.Headers.Authorization; diff --git a/src/Serilog.Ui.Web/Authorization/Filters/IBasicAuthenticationService.cs b/src/Serilog.Ui.Web/Authorization/Filters/IBasicAuthenticationService.cs index 3a9e20a3..c1e28bc7 100644 --- a/src/Serilog.Ui.Web/Authorization/Filters/IBasicAuthenticationService.cs +++ b/src/Serilog.Ui.Web/Authorization/Filters/IBasicAuthenticationService.cs @@ -1,46 +1,16 @@ -using System; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; +using System.Net.Http.Headers; namespace Serilog.Ui.Web.Authorization.Filters; +/// +/// Provides basic authentication services. +/// public interface IBasicAuthenticationService { + /// + /// Determines whether access is granted based on the provided basic authentication header. + /// + /// The basic authentication header containing the credentials. + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether access is granted. Task CanAccessAsync(AuthenticationHeaderValue basicHeader); -} - -internal class BasicAuthServiceByConfiguration(IConfiguration configuration) : IBasicAuthenticationService -{ - private string? UserName { get; } = configuration["SerilogUi:UserName"]; - - private string? Password { get; } = configuration["SerilogUi:Password"]; - - public Task CanAccessAsync(AuthenticationHeaderValue basicHeader) - { - var header = basicHeader.Parameter; - - return Task.FromResult(EvaluateAuthResult(header)); - } - - private bool EvaluateAuthResult(string? header) - { - var tokens = ExtractAuthenticationTokens(header); - var matchCredentials = CredentialsMatch(tokens); - - return matchCredentials; - } - - private static (string, string) ExtractAuthenticationTokens(string? authValues) - { - var parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authValues ?? string.Empty)); - var parts = parameter.Split(':'); - return (parts[0], parts[1]); - } - - private bool CredentialsMatch((string Username, string Password) tokens) - { - return tokens.Username == UserName && tokens.Password == Password; - } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/Filters/LocalRequestsOnlyAuthorizationFilter.cs b/src/Serilog.Ui.Web/Authorization/Filters/LocalRequestsOnlyAuthorizationFilter.cs index da0ebbd5..518ad552 100644 --- a/src/Serilog.Ui.Web/Authorization/Filters/LocalRequestsOnlyAuthorizationFilter.cs +++ b/src/Serilog.Ui.Web/Authorization/Filters/LocalRequestsOnlyAuthorizationFilter.cs @@ -1,16 +1,11 @@ -using Microsoft.AspNetCore.Http; -using Serilog.Ui.Core.Interfaces; +using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Web.Extensions; -namespace Serilog.Ui.Web.Authorization.Filters -{ - internal class LocalRequestsOnlyAuthorizationFilter(IHttpContextAccessor httpContextAccessor) : IUiAuthorizationFilter - { - public bool Authorize() - { - var httpContext = httpContextAccessor.HttpContext; +namespace Serilog.Ui.Web.Authorization.Filters; - return httpContext is not null && httpContext.Request.IsLocal(); - } - } +internal class LocalRequestsOnlyAuthorizationFilter(IHttpContextAccessor httpContextAccessor) : IUiAuthorizationFilter +{ + public bool Authorize() => + httpContextAccessor.HttpContext is not null && + httpContextAccessor.HttpContext.Request.IsLocal(); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/Filters/PolicyAuthorizationFilter.cs b/src/Serilog.Ui.Web/Authorization/Filters/PolicyAuthorizationFilter.cs index 8c044d4d..9dacb9c4 100644 --- a/src/Serilog.Ui.Web/Authorization/Filters/PolicyAuthorizationFilter.cs +++ b/src/Serilog.Ui.Web/Authorization/Filters/PolicyAuthorizationFilter.cs @@ -1,6 +1,4 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; using Serilog.Ui.Core.Interfaces; namespace Serilog.Ui.Web.Authorization.Filters; @@ -8,15 +6,18 @@ namespace Serilog.Ui.Web.Authorization.Filters; internal class PolicyAuthorizationFilter( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService, - string policy) - : IUiAsyncAuthorizationFilter + string policy + ) : IUiAsyncAuthorizationFilter { public async Task AuthorizeAsync() { var httpContext = httpContextAccessor.HttpContext; - if (httpContext is null) return false; + if (httpContext is null) + { + return false; + } - var result = await authorizationService.AuthorizeAsync(httpContext.User, policy); + AuthorizationResult result = await authorizationService.AuthorizeAsync(httpContext.User, policy); return result.Succeeded; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Authorization/IAuthorizationFilterService.cs b/src/Serilog.Ui.Web/Authorization/IAuthorizationFilterService.cs index 918f0042..c6053b2b 100644 --- a/src/Serilog.Ui.Web/Authorization/IAuthorizationFilterService.cs +++ b/src/Serilog.Ui.Web/Authorization/IAuthorizationFilterService.cs @@ -1,10 +1,15 @@ -using System; -using System.Threading.Tasks; +namespace Serilog.Ui.Web.Authorization; -namespace Serilog.Ui.Web.Authorization +/// +/// Provides services for authorization filtering. +/// +internal interface IAuthorizationFilterService { - internal interface IAuthorizationFilterService - { - Task CheckAccessAsync(Func onSuccess, Func? onFailure = null); - } + /// + /// Checks access and executes the appropriate callback based on the result. + /// + /// The callback to execute if access is granted. + /// The optional callback to execute if access is denied. + /// A task that represents the asynchronous operation. + Task CheckAccessAsync(Func onSuccess, Func? onFailure = null); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/AppStreamLoader.cs b/src/Serilog.Ui.Web/Endpoints/AppStreamLoader.cs index 4589458f..de69814f 100644 --- a/src/Serilog.Ui.Web/Endpoints/AppStreamLoader.cs +++ b/src/Serilog.Ui.Web/Endpoints/AppStreamLoader.cs @@ -1,15 +1,13 @@ -using System.IO; -using Serilog.Ui.Web.Models; +using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints +namespace Serilog.Ui.Web.Endpoints; + +internal class AppStreamLoader : IAppStreamLoader { - internal class AppStreamLoader : IAppStreamLoader - { - private const string AppManifest = "Serilog.Ui.Web.wwwroot.dist.index.html"; + private const string AppManifest = "Serilog.Ui.Web.wwwroot.dist.index.html"; - public Stream? GetIndex() => - typeof(AuthorizationOptions) - .Assembly - .GetManifestResourceStream(AppManifest); - } + public Stream? GetIndex() => + typeof(AuthorizationOptions) + .Assembly + .GetManifestResourceStream(AppManifest); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/IAppStreamLoader.cs b/src/Serilog.Ui.Web/Endpoints/IAppStreamLoader.cs index 154dfaf0..89432635 100644 --- a/src/Serilog.Ui.Web/Endpoints/IAppStreamLoader.cs +++ b/src/Serilog.Ui.Web/Endpoints/IAppStreamLoader.cs @@ -1,9 +1,13 @@ -using System.IO; +namespace Serilog.Ui.Web.Endpoints; -namespace Serilog.Ui.Web.Endpoints +/// +/// Provides methods to load application streams. +/// +internal interface IAppStreamLoader { - internal interface IAppStreamLoader - { - Stream? GetIndex(); - } + /// + /// Gets the index stream. + /// + /// The index stream, or null if the stream is not available. + Stream? GetIndex(); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/ISerilogUiAppRoutes.cs b/src/Serilog.Ui.Web/Endpoints/ISerilogUiAppRoutes.cs index d2c8c647..3576ba78 100644 --- a/src/Serilog.Ui.Web/Endpoints/ISerilogUiAppRoutes.cs +++ b/src/Serilog.Ui.Web/Endpoints/ISerilogUiAppRoutes.cs @@ -1,13 +1,24 @@ -using System.Threading.Tasks; +namespace Serilog.Ui.Web.Endpoints; -namespace Serilog.Ui.Web.Endpoints +/// +/// Provides application routes for Serilog UI and inherits methods to set options. +/// +public interface ISerilogUiAppRoutes : ISerilogUiOptionsSetter { - public interface ISerilogUiAppRoutes : ISerilogUiOptionsSetter - { - protected internal bool BlockHomeAccess { get; set; } + /// + /// Gets or sets a value indicating whether access to the home route is blocked. + /// + protected internal bool BlockHomeAccess { get; set; } - Task GetHomeAsync(); + /// + /// Asynchronously retrieves the home page. + /// + /// A task that represents the asynchronous operation. + Task GetHomeAsync(); - Task RedirectHomeAsync(); - } + /// + /// Asynchronously redirects to the home page. + /// + /// A task that represents the asynchronous operation. + Task RedirectHomeAsync(); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/ISerilogUiEndpoints.cs b/src/Serilog.Ui.Web/Endpoints/ISerilogUiEndpoints.cs index 871ba767..8e59eba8 100644 --- a/src/Serilog.Ui.Web/Endpoints/ISerilogUiEndpoints.cs +++ b/src/Serilog.Ui.Web/Endpoints/ISerilogUiEndpoints.cs @@ -1,11 +1,19 @@ -using System.Threading.Tasks; +namespace Serilog.Ui.Web.Endpoints; -namespace Serilog.Ui.Web.Endpoints +/// +/// Provides endpoints for Serilog UI and inherits methods to set options. +/// +public interface ISerilogUiEndpoints : ISerilogUiOptionsSetter { - public interface ISerilogUiEndpoints : ISerilogUiOptionsSetter - { - Task GetApiKeysAsync(); + /// + /// Asynchronously retrieves API keys. + /// + /// A task that represents the asynchronous operation. + Task GetApiKeysAsync(); - Task GetLogsAsync(); - } -} + /// + /// Asynchronously retrieves logs. + /// + /// A task that represents the asynchronous operation. + Task GetLogsAsync(); +} \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/ISerilogUiOptionsSetter.cs b/src/Serilog.Ui.Web/Endpoints/ISerilogUiOptionsSetter.cs index 6b28f955..b4a83b90 100644 --- a/src/Serilog.Ui.Web/Endpoints/ISerilogUiOptionsSetter.cs +++ b/src/Serilog.Ui.Web/Endpoints/ISerilogUiOptionsSetter.cs @@ -1,11 +1,20 @@ using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints +namespace Serilog.Ui.Web.Endpoints; + +/// +/// Provides methods to set Serilog UI options. +/// +public interface ISerilogUiOptionsSetter { - public interface ISerilogUiOptionsSetter - { - UiOptions? Options { get; } + /// + /// Gets the current UI options. + /// + UiOptions? Options { get; } - void SetOptions(UiOptions options); - } + /// + /// Sets the UI options. + /// + /// The UI options to set. + void SetOptions(UiOptions options); } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/JsonSerializerOptionsFactory.cs b/src/Serilog.Ui.Web/Endpoints/JsonSerializerOptionsFactory.cs new file mode 100644 index 00000000..f4c11957 --- /dev/null +++ b/src/Serilog.Ui.Web/Endpoints/JsonSerializerOptionsFactory.cs @@ -0,0 +1,10 @@ +namespace Serilog.Ui.Web.Endpoints; + +internal static class JsonSerializerOptionsFactory +{ + public static JsonSerializerOptions GetDefaultOptions => new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; +} \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutes.cs b/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutes.cs index e31131e4..26d7acac 100644 --- a/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutes.cs +++ b/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutes.cs @@ -1,93 +1,78 @@ -using System; -using System.IO; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Ardalis.GuardClauses; -using Microsoft.AspNetCore.Http; +using System.Text; using Microsoft.AspNetCore.Http.Extensions; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints -{ - internal class SerilogUiAppRoutes( - IHttpContextAccessor httpContextAccessor, - IAppStreamLoader appStreamLoader) : ISerilogUiAppRoutes - { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; +namespace Serilog.Ui.Web.Endpoints; - public UiOptions? Options { get; private set; } +internal class SerilogUiAppRoutes(IHttpContextAccessor httpContextAccessor, IAppStreamLoader appStreamLoader) + : ISerilogUiAppRoutes +{ + private readonly HttpContext _httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); - public bool BlockHomeAccess { get; set; } + public bool BlockHomeAccess { get; set; } - public async Task GetHomeAsync() - { - Guard.Against.Null(Options, nameof(Options)); - var httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); + public UiOptions? Options { get; private set; } - var response = httpContext.Response; + public void SetOptions(UiOptions options) + { + Options = options; + } - await using var stream = appStreamLoader.GetIndex(); - if (stream is null) - { - response.StatusCode = 500; - await response.WriteAsync("
Server error while loading assets. Please contact administration.
", Encoding.UTF8); - return; - } + public async Task GetHomeAsync() + { + Guard.Against.Null(Options, nameof(Options)); - var htmlString = await LoadStream(stream, Options); - response.StatusCode = 200; - response.ContentType = "text/html;charset=utf-8"; + var response = _httpContext.Response; - await response.WriteAsync(htmlString, Encoding.UTF8); + await using Stream? stream = appStreamLoader.GetIndex(); + if (stream is null) + { + response.StatusCode = 500; + await response.WriteAsync("
Server error while loading assets. Please contact administration.
", Encoding.UTF8); + return; } - public Task RedirectHomeAsync() - { - var httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); + response.StatusCode = 200; + response.ContentType = "text/html;charset=utf-8"; + string htmlString = await LoadStream(stream, Options); - var indexUrl = httpContext.Request.GetEncodedUrl().Replace("index.html", ""); - var indexUrlWithTrailingSlash = indexUrl.EndsWith('/') ? indexUrl : $"{indexUrl}/"; + await response.WriteAsync(htmlString, Encoding.UTF8); + } - httpContext.Response.Redirect(indexUrlWithTrailingSlash, true); + public Task RedirectHomeAsync() + { + string indexUrl = _httpContext.Request.GetEncodedUrl().Replace("index.html", ""); + string indexUrlWithTrailingSlash = indexUrl.EndsWith('/') ? indexUrl : $"{indexUrl}/"; - return Task.CompletedTask; - } + _httpContext.Response.Redirect(indexUrlWithTrailingSlash, true); - public void SetOptions(UiOptions options) - { - Options = options; - } + return Task.CompletedTask; + } - private async Task LoadStream(Stream stream, UiOptions options) + private async Task LoadStream(Stream stream, UiOptions options) + { + StringBuilder htmlStringBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync()); + string authType = options.Authorization.AuthenticationType.ToString(); + + var feOpts = new { - var htmlStringBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync()); - var authType = options.Authorization.AuthenticationType.ToString(); - var feOpts = new - { - authType, - options.ColumnsInfo, - options.DisabledSortOnKeys, - options.RenderExceptionAsStringKeys, - options.ShowBrand, - options.HomeUrl, - BlockHomeAccess, - options.RoutePrefix, - options.ExpandDropdownsByDefault - }; - var encodeAuthOpts = Uri.EscapeDataString(JsonSerializer.Serialize(feOpts, JsonSerializerOptions)); - - htmlStringBuilder - .Replace("%(Configs)", encodeAuthOpts) - .Replace("", options.HeadContent) - .Replace("", options.BodyContent); - - return htmlStringBuilder.ToString(); - } + authType, + options.ColumnsInfo, + options.DisabledSortOnKeys, + options.RenderExceptionAsStringKeys, + options.ShowBrand, + options.HomeUrl, + BlockHomeAccess, + options.RoutePrefix, + options.ExpandDropdownsByDefault + }; + string encodeAuthOpts = Uri.EscapeDataString(JsonSerializer.Serialize(feOpts, JsonSerializerOptionsFactory.GetDefaultOptions)); + + htmlStringBuilder + .Replace("%(Configs)", encodeAuthOpts) + .Replace("", options.HeadContent) + .Replace("", options.BodyContent); + + return htmlStringBuilder.ToString(); } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutesDecorator.cs b/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutesDecorator.cs index 2369cf2f..b1f4aa05 100644 --- a/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutesDecorator.cs +++ b/src/Serilog.Ui.Web/Endpoints/SerilogUiAppRoutesDecorator.cs @@ -1,54 +1,50 @@ -using System.Net; -using System.Threading.Tasks; -using Ardalis.GuardClauses; -using Microsoft.AspNetCore.Http; -using Serilog.Ui.Web.Authorization; +using Serilog.Ui.Web.Authorization; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints -{ - internal class SerilogUiAppRoutesDecorator( - IHttpContextAccessor httpContextAccessor, - ISerilogUiAppRoutes decoratedService, - IAuthorizationFilterService authFilterService) : ISerilogUiAppRoutes - { - public UiOptions? Options { get; private set; } +namespace Serilog.Ui.Web.Endpoints; - public bool BlockHomeAccess { get; set; } - - public async Task GetHomeAsync() - { - Guard.Against.Null(Options, nameof(Options)); - Guard.Against.Null(httpContextAccessor.HttpContext); +internal class SerilogUiAppRoutesDecorator( + IHttpContextAccessor httpContextAccessor, + ISerilogUiAppRoutes decoratedService, + IAuthorizationFilterService authFilterService + ) : ISerilogUiAppRoutes +{ + private readonly HttpContext _httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); - if (Options.Authorization.RunAuthorizationFilterOnAppRoutes) - { - await authFilterService.CheckAccessAsync(() => Task.CompletedTask, OnAccessFailure); - } + public bool BlockHomeAccess { get; set; } - await decoratedService.GetHomeAsync(); + public UiOptions? Options { get; private set; } - Task OnAccessFailure() - { - httpContextAccessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK; + public void SetOptions(UiOptions options) + { + Options = options; + decoratedService.SetOptions(options); + } - decoratedService.BlockHomeAccess = true; + public async Task GetHomeAsync() + { + Guard.Against.Null(Options, nameof(Options)); - return Task.CompletedTask; - } + if (Options.Authorization.RunAuthorizationFilterOnAppRoutes) + { + await authFilterService.CheckAccessAsync(() => Task.CompletedTask, OnAccessFailure); } - public Task RedirectHomeAsync() + await decoratedService.GetHomeAsync(); + + Task OnAccessFailure() { - Guard.Against.Null(Options, nameof(Options)); + _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; + decoratedService.BlockHomeAccess = true; - return decoratedService.RedirectHomeAsync(); + return Task.CompletedTask; } + } - public void SetOptions(UiOptions options) - { - Options = options; - decoratedService.SetOptions(options); - } + public Task RedirectHomeAsync() + { + Guard.Against.Null(Options, nameof(Options)); + + return decoratedService.RedirectHomeAsync(); } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpoints.cs b/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpoints.cs index 17b1295d..055e7a7d 100644 --- a/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpoints.cs +++ b/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpoints.cs @@ -1,150 +1,120 @@ -using System; -using System.Linq; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Ardalis.GuardClauses; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Serilog.Ui.Core; +using Serilog.Ui.Core; using Serilog.Ui.Core.Models; using Serilog.Ui.Web.Extensions; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints -{ - internal class SerilogUiEndpoints : ISerilogUiEndpoints - { - private readonly IHttpContextAccessor _httpContextAccessor; - - private readonly ILogger _logger; +namespace Serilog.Ui.Web.Endpoints; - private readonly AggregateDataProvider _aggregateDataProvider; +internal class SerilogUiEndpoints( + IHttpContextAccessor httpContextAccessor, + ILogger logger, + AggregateDataProvider aggregateDataProvider + ) : ISerilogUiEndpoints +{ + private readonly HttpContext _httpContext = Guard.Against.Null(httpContextAccessor.HttpContext); + private readonly string[] _providerKeys = aggregateDataProvider.Keys.ToArray(); - public SerilogUiEndpoints(IHttpContextAccessor httpContextAccessor, - ILogger logger, - AggregateDataProvider aggregateDataProvider) - { - _httpContextAccessor = httpContextAccessor; - _logger = logger; - _aggregateDataProvider = aggregateDataProvider; + public UiOptions? Options { get; private set; } - _providerKeys = _aggregateDataProvider.Keys.ToArray(); - } + public void SetOptions(UiOptions options) + { + Options = options; + } - private static readonly JsonSerializerOptions JsonSerializerOptions = new() + public async Task GetApiKeysAsync() + { + try { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - private readonly string[] _providerKeys; + SetResponseContentType(); - public UiOptions? Options { get; private set; } + string result = JsonSerializer.Serialize(_providerKeys, JsonSerializerOptionsFactory.GetDefaultOptions); + _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; - public async Task GetApiKeysAsync() + await _httpContext.Response.WriteAsync(result); + } + catch (Exception ex) { - var httpContext = Guard.Against.Null(_httpContextAccessor.HttpContext); - - try - { - SetResponseContentType(httpContext); - - var result = JsonSerializer.Serialize(_providerKeys, JsonSerializerOptions); - httpContext.Response.StatusCode = (int)HttpStatusCode.OK; - await httpContext.Response.WriteAsync(result); - } - catch (Exception ex) - { - await OnError(httpContext, ex); - } + await OnError(ex); } + } - public async Task GetLogsAsync() + public async Task GetLogsAsync() + { + try { - var httpContext = Guard.Against.Null(_httpContextAccessor.HttpContext); + SetResponseContentType(); - try - { - SetResponseContentType(httpContext); + string result = await FetchLogsAsync(); + _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; - var result = await FetchLogsAsync(httpContext); - httpContext.Response.StatusCode = (int)HttpStatusCode.OK; - await httpContext.Response.WriteAsync(result); - } - catch (Exception ex) - { - await OnError(httpContext, ex); - } + await _httpContext.Response.WriteAsync(result); } - - public void SetOptions(UiOptions options) + catch (Exception ex) { - Options = options; + await OnError(ex); } + } - private async Task FetchLogsAsync(HttpContext httpContext) + private async Task FetchLogsAsync() + { + Dictionary queryDictionary = QueryHelpers.ParseQuery(_httpContext.Request.QueryString.Value); + FetchLogsQuery queryLogs = FetchLogsQuery.ParseQuery(queryDictionary); + + if (!string.IsNullOrWhiteSpace(queryLogs.DatabaseKey)) { - var queryDictionary = QueryHelpers.ParseQuery(httpContext.Request.QueryString.Value); - var queryLogs = FetchLogsQuery.ParseQuery(queryDictionary); + aggregateDataProvider.SwitchToProvider(queryLogs.DatabaseKey); + } - if (!string.IsNullOrWhiteSpace(queryLogs.DatabaseKey)) - { - _aggregateDataProvider.SwitchToProvider(queryLogs.DatabaseKey); - } + (IEnumerable logs, int total) = await aggregateDataProvider.FetchDataAsync(queryLogs); - var (logs, total) = await _aggregateDataProvider.FetchDataAsync(queryLogs); + // due to System.Text.Json design choice: + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism + IEnumerable serializeLogs = logs.ToList(); - // due to System.Text.Json design choice: - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism - var serializeLogs = logs.ToList().Cast(); + string result = JsonSerializer.Serialize(new + { + logs = serializeLogs, + total, + count = queryLogs.Count, + currentPage = queryLogs.CurrentPage + }, JsonSerializerOptionsFactory.GetDefaultOptions); - var result = JsonSerializer.Serialize(new { logs = serializeLogs, total, count = queryLogs.Count, currentPage = queryLogs.CurrentPage }, - JsonSerializerOptions); + return result; + } - return result; - } + /// + /// Returns an industry standard ProblemDetails object. + /// See: + /// + /// + /// + private Task OnError(Exception ex) + { + logger.LogError(ex, "{Message}", ex.Message); - /// - /// Returns an industry standard ProblemDetails object. - /// See: - /// - /// - /// - /// - private Task OnError(HttpContext httpContext, Exception ex) - { - _logger.LogError(ex, "{Message}", ex.Message); + _httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + _httpContext.Response.ContentType = "application/problem+json"; - httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - httpContext.Response.ContentType = "application/problem+json"; + bool includeDetails = _httpContext.Request.IsLocal(); - var includeDetails = httpContext.Request.IsLocal(); + ProblemDetails problem = new() + { + Status = _httpContext.Response.StatusCode, + Title = includeDetails ? $"An error occured: {ex.Message}" : "An error occured", + Detail = includeDetails ? ex.ToString() : null, + Extensions = + { + ["traceId"] = _httpContext.TraceIdentifier + } + }; - var title = includeDetails ? "An error occured: " + ex.Message : "An error occured"; - var details = includeDetails ? ex.ToString() : null; + Stream stream = _httpContext.Response.Body; - var problem = new ProblemDetails - { - Status = httpContext.Response.StatusCode, - Title = title, - Detail = details, - Extensions = - { - ["traceId"] = httpContext.TraceIdentifier - } - }; - - var stream = httpContext.Response.Body; - return JsonSerializer.SerializeAsync(stream, problem); - } + return JsonSerializer.SerializeAsync(stream, problem); + } - private static void SetResponseContentType(HttpContext httpContext) - { - httpContext.Response.ContentType = "application/json;charset=utf-8"; - } + private void SetResponseContentType() + { + _httpContext.Response.ContentType = "application/json;charset=utf-8"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpointsDecorator.cs b/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpointsDecorator.cs index ce0093a1..9ce7b5de 100644 --- a/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpointsDecorator.cs +++ b/src/Serilog.Ui.Web/Endpoints/SerilogUiEndpointsDecorator.cs @@ -1,28 +1,26 @@ -using System.Threading.Tasks; -using Serilog.Ui.Web.Authorization; +using Serilog.Ui.Web.Authorization; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Endpoints +namespace Serilog.Ui.Web.Endpoints; + +internal class SerilogUiEndpointsDecorator(ISerilogUiEndpoints decoratedService, IAuthorizationFilterService authFilterService) + : ISerilogUiEndpoints { - internal class SerilogUiEndpointsDecorator(ISerilogUiEndpoints decoratedService, IAuthorizationFilterService authFilterService) - : ISerilogUiEndpoints - { - public UiOptions? Options { get; private set; } + public UiOptions? Options { get; private set; } - public Task GetApiKeysAsync() - { - return authFilterService.CheckAccessAsync(decoratedService.GetApiKeysAsync); - } + public void SetOptions(UiOptions options) + { + Options = options; + decoratedService.SetOptions(options); + } - public Task GetLogsAsync() - { - return authFilterService.CheckAccessAsync(decoratedService.GetLogsAsync); - } + public Task GetApiKeysAsync() + { + return authFilterService.CheckAccessAsync(decoratedService.GetApiKeysAsync); + } - public void SetOptions(UiOptions options) - { - Options = options; - decoratedService.SetOptions(options); - } + public Task GetLogsAsync() + { + return authFilterService.CheckAccessAsync(decoratedService.GetLogsAsync); } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Extensions/ApplicationBuilderExtensions.cs b/src/Serilog.Ui.Web/Extensions/ApplicationBuilderExtensions.cs index 4a2e9009..88e77fa5 100644 --- a/src/Serilog.Ui.Web/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Serilog.Ui.Web/Extensions/ApplicationBuilderExtensions.cs @@ -1,37 +1,34 @@ -using System; -using Ardalis.GuardClauses; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core.Models.Options; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web.Extensions +namespace Serilog.Ui.Web.Extensions; + +/// +/// Contains extensions for configuring routing on an . +/// +public static class ApplicationBuilderExtensions { /// - /// Contains extensions for configuring routing on an . + /// Adds a middleware to the specified . /// - public static class ApplicationBuilderExtensions + /// + /// The to add the middleware to. + /// + /// The options to configure Serilog UI dashboard. + /// IApplicationBuilder. + /// throw if applicationBuilder is null + /// if validation fails + public static IApplicationBuilder UseSerilogUi(this IApplicationBuilder applicationBuilder, Action? options = null) { - /// - /// Adds a middleware to the specified . - /// - /// - /// The to add the middleware to. - /// - /// The options to configure Serilog UI dashboard. - /// IApplicationBuilder. - /// throw if applicationBuilder is null - /// if validation fails - public static IApplicationBuilder UseSerilogUi(this IApplicationBuilder applicationBuilder, Action? options = null) - { - Guard.Against.Null(applicationBuilder); + Guard.Against.Null(applicationBuilder); - var providerOptions = applicationBuilder.ApplicationServices.GetRequiredService(); - var uiOptions = new UiOptions(providerOptions); - options?.Invoke(uiOptions); - uiOptions.Validate(); + ProvidersOptions providerOptions = applicationBuilder.ApplicationServices.GetRequiredService(); + UiOptions uiOptions = new UiOptions(providerOptions); + options?.Invoke(uiOptions); + uiOptions.Validate(); - return applicationBuilder.UseMiddleware(uiOptions); - } + return applicationBuilder.UseMiddleware(uiOptions); } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Extensions/HttpRequestExtensions.cs b/src/Serilog.Ui.Web/Extensions/HttpRequestExtensions.cs index ecd74c7f..68e261b1 100644 --- a/src/Serilog.Ui.Web/Extensions/HttpRequestExtensions.cs +++ b/src/Serilog.Ui.Web/Extensions/HttpRequestExtensions.cs @@ -1,29 +1,24 @@ -using System.Linq; -using System.Net; -using Microsoft.AspNetCore.Http; +namespace Serilog.Ui.Web.Extensions; -namespace Serilog.Ui.Web.Extensions +public static class HttpRequestExtensions { - public static class HttpRequestExtensions + public static bool IsLocal(this HttpRequest request) { - public static bool IsLocal(this HttpRequest request) + var ipAddress = request.Headers["X-forwarded-for"].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(ipAddress)) { - var ipAddress = request.Headers["X-forwarded-for"].FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(ipAddress)) - { - return false; - } - - var connection = request.HttpContext.Connection; - if (connection.RemoteIpAddress != null) - { - return connection.LocalIpAddress != null - ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) - : IPAddress.IsLoopback(connection.RemoteIpAddress); - } + return false; + } - // we know remote ip is null, thus it can be only be local - return true; + var connection = request.HttpContext.Connection; + if (connection.RemoteIpAddress != null) + { + return connection.LocalIpAddress != null + ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) + : IPAddress.IsLoopback(connection.RemoteIpAddress); } + + // we know remote ip is null, thus it can be only be local + return true; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs index 42b768b6..469b0d7b 100644 --- a/src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core.Interfaces; diff --git a/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs b/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs index 11254434..03b6546b 100644 --- a/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs +++ b/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs @@ -1,57 +1,53 @@ -using System; -using System.Linq; -using Ardalis.GuardClauses; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.OptionsBuilder; using Serilog.Ui.Web.Authorization; using Serilog.Ui.Web.Endpoints; -namespace Serilog.Ui.Web.Extensions +namespace Serilog.Ui.Web.Extensions; + +/// +/// Extension methods for setting up SerilogUI related services in an . +/// +public static class ServiceCollectionExtensions { /// - /// Extension methods for setting up SerilogUI related services in an . + /// Registers the SerilogUI as a service in the . /// - public static class ServiceCollectionExtensions + /// The services. + /// + /// An action to configure the for the SerilogUI. + /// + /// services + /// optionsBuilder + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddSerilogUi(this IServiceCollection services, Action optionsBuilder) { - /// - /// Registers the SerilogUI as a service in the . - /// - /// The services. - /// - /// An action to configure the for the SerilogUI. - /// - /// services - /// optionsBuilder - /// The same service collection so that multiple calls can be chained. - public static IServiceCollection AddSerilogUi(this IServiceCollection services, Action optionsBuilder) + Guard.Against.Null(services, nameof(services)); + Guard.Against.Null(optionsBuilder, nameof(optionsBuilder)); + var isProviderAlreadyRegistered = services.Any(c => c.ImplementationType == typeof(AggregateDataProvider)); + if (isProviderAlreadyRegistered) { - Guard.Against.Null(services, nameof(services)); - Guard.Against.Null(optionsBuilder, nameof(optionsBuilder)); - var isProviderAlreadyRegistered = services.Any(c => c.ImplementationType == typeof(AggregateDataProvider)); - if (isProviderAlreadyRegistered) - { - throw new InvalidOperationException($"{nameof(AddSerilogUi)} can be invoked one time per service registration."); - } + throw new InvalidOperationException($"{nameof(AddSerilogUi)} can be invoked one time per service registration."); + } - var builder = new SerilogUiOptionsBuilder(services); - optionsBuilder.Invoke(builder); - builder.RegisterProviderServices(); + var builder = new SerilogUiOptionsBuilder(services); + optionsBuilder.Invoke(builder); + builder.RegisterProviderServices(); - services.AddHttpContextAccessor(); - services.AddScoped(); - services.AddSingleton(); + services.AddHttpContextAccessor(); + services.AddScoped(); + services.AddSingleton(); - services.AddScoped(); - services.Decorate(); + services.AddScoped(); + services.Decorate(); - services.AddScoped(); - services.Decorate(); + services.AddScoped(); + services.Decorate(); - services.AddScoped(); + services.AddScoped(); - return services; - } + return services; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/GlobalUsings.cs b/src/Serilog.Ui.Web/GlobalUsings.cs new file mode 100644 index 00000000..0efad633 --- /dev/null +++ b/src/Serilog.Ui.Web/GlobalUsings.cs @@ -0,0 +1,14 @@ +global using Ardalis.GuardClauses; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.WebUtilities; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Primitives; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Models/AuthenticationType.cs b/src/Serilog.Ui.Web/Models/AuthenticationType.cs new file mode 100644 index 00000000..13810438 --- /dev/null +++ b/src/Serilog.Ui.Web/Models/AuthenticationType.cs @@ -0,0 +1,8 @@ +namespace Serilog.Ui.Web.Models; + +public enum AuthenticationType +{ + Custom, + Basic, + Jwt +} \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Models/AuthorizationOptions.cs b/src/Serilog.Ui.Web/Models/AuthorizationOptions.cs index c5692083..a87f5e87 100644 --- a/src/Serilog.Ui.Web/Models/AuthorizationOptions.cs +++ b/src/Serilog.Ui.Web/Models/AuthorizationOptions.cs @@ -1,30 +1,20 @@ -namespace Serilog.Ui.Web.Models +namespace Serilog.Ui.Web.Models; + +/// +/// The options to be used by SerilogUI to log access authorization. +/// +internal class AuthorizationOptions { /// - /// The options to be used by SerilogUI to log access authorization. + /// Gets the type of the authentication. Defaults to . /// - internal class AuthorizationOptions - { - /// - /// Gets the type of the authentication. Defaults to . - /// - /// The type of the authentication. - public AuthenticationType AuthenticationType { get; set; } = AuthenticationType.Custom; - - /// - /// Gets if the authorization filters should be run when accessing the serilog-ui main pages. - /// Used to block unauthorized access to the ui. - /// Defaults to false. - /// - public bool RunAuthorizationFilterOnAppRoutes { get; set; } - } - - public enum AuthenticationType - { - Custom, + /// The type of the authentication. + public AuthenticationType AuthenticationType { get; set; } = AuthenticationType.Custom; - Basic, - - Jwt - } + /// + /// Gets if the authorization filters should be run when accessing the serilog-ui main pages. + /// Used to block unauthorized access to the ui. + /// Defaults to false. + /// + public bool RunAuthorizationFilterOnAppRoutes { get; set; } } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Models/UiOptions.cs b/src/Serilog.Ui.Web/Models/UiOptions.cs index 50d5e4c4..b9820cf6 100644 --- a/src/Serilog.Ui.Web/Models/UiOptions.cs +++ b/src/Serilog.Ui.Web/Models/UiOptions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Text; using Serilog.Ui.Core.Models; using Serilog.Ui.Core.Models.Options; @@ -70,10 +68,9 @@ public UiOptions WithExpandedDropdownsByDefault() return this; } - /// /// Injects additional CSS stylesheets into the index.html page. - /// Each call to the method adds a stylesheet entry. + /// Each call to the method adds a stylesheet entry. /// /// A path to the stylesheet - i.e. the link "href" attribute /// The target media - i.e. the link "media" attribute @@ -168,5 +165,5 @@ internal void Validate() /// The head content. internal string HeadContent { get; set; } = string.Empty; - #endregion + #endregion internals } \ No newline at end of file diff --git a/src/Serilog.Ui.Web/SerilogUiMiddleware.cs b/src/Serilog.Ui.Web/SerilogUiMiddleware.cs index 2f175240..e3c5e966 100644 --- a/src/Serilog.Ui.Web/SerilogUiMiddleware.cs +++ b/src/Serilog.Ui.Web/SerilogUiMiddleware.cs @@ -1,106 +1,111 @@ -using System; using System.Reflection; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog.Ui.Web.Endpoints; using Serilog.Ui.Web.Models; -namespace Serilog.Ui.Web +namespace Serilog.Ui.Web; + +public class SerilogUiMiddleware { - public class SerilogUiMiddleware + private const string EmbeddedFileNamespace = "Serilog.Ui.Web.wwwroot.dist"; + private readonly UiOptions _options; + private readonly StaticFileMiddleware _staticFileMiddleware; + + public SerilogUiMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + UiOptions options) { - private const string EmbeddedFileNamespace = "Serilog.Ui.Web.wwwroot.dist"; - - private readonly UiOptions _options; + _options = options; + _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory); + } - private readonly StaticFileMiddleware _staticFileMiddleware; + public Task Invoke(HttpContext httpContext, ISerilogUiAppRoutes uiAppRoutes, ISerilogUiEndpoints uiEndpoints) + { + string? path = httpContext.Request.Path.Value; + string httpMethod = httpContext.Request.Method; + bool isGet = httpMethod == "GET"; - public SerilogUiMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - UiOptions options) + if (!isGet || path is null) { - _options = options; - _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory); + return _staticFileMiddleware.Invoke(httpContext); } - public Task Invoke(HttpContext httpContext, ISerilogUiAppRoutes uiAppRoutes, ISerilogUiEndpoints uiEndpoints) - { - var path = httpContext.Request.Path.Value; - var httpMethod = httpContext.Request.Method; - var isGet = httpMethod == "GET"; - - if (!isGet || path is null) - { - return _staticFileMiddleware.Invoke(httpContext); - } + uiAppRoutes.SetOptions(_options); + uiEndpoints.SetOptions(_options); - uiAppRoutes.SetOptions(_options); - uiEndpoints.SetOptions(_options); - - if (CheckPath(path, "/api/keys/?")) return uiEndpoints.GetApiKeysAsync(); - if (CheckPath(path, "/api/logs/?")) return uiEndpoints.GetLogsAsync(); - - // prefix without trailing slash or old index.html routing - if (CheckPath(path, "/index.html") || CheckPath(path, "")) return uiAppRoutes.RedirectHomeAsync(); - - // asset request, we remove any extra path part since it's always served from the serilog-ui root - if (CheckPath(path, "/(?:.*(.*/))(?:(assets/)).*")) return ChangeAssetRequestPath(httpContext); + if (CheckPath(path, "/api/keys/?")) + { + return uiEndpoints.GetApiKeysAsync(); + } - return CheckPath(path, "/(?!.*(assets/)).*") ? uiAppRoutes.GetHomeAsync() : _staticFileMiddleware.Invoke(httpContext); + if (CheckPath(path, "/api/logs/?")) + { + return uiEndpoints.GetLogsAsync(); } - private StaticFileMiddleware CreateStaticFileMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory) + // prefix without trailing slash or old index.html routing + if (CheckPath(path, "/index.html") || CheckPath(path, "")) { - var staticFileOptions = new StaticFileOptions - { - RequestPath = $"/{_options.RoutePrefix}", - FileProvider = new EmbeddedFileProvider(typeof(SerilogUiMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), - }; + return uiAppRoutes.RedirectHomeAsync(); + } - return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + // asset request, we remove any extra path part since it's always served from the serilog-ui root + if (CheckPath(path, "/(?:.*(.*/))(?:(assets/)).*")) + { + return ChangeAssetRequestPath(httpContext); } - private bool CheckPath(string currentPath, string onPath) - => Regex.IsMatch(currentPath, $"^/{Regex.Escape(_options.RoutePrefix)}{onPath}$", RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5)); - - /// - /// Since the FE app includes compiled js assets as relative urls, when the page has sub-paths the app tries to access the asset - /// from the whole sub-paths url. - ///
- /// The StaticFileMiddleware always expose it from the serilog-ui route prefix. We remove the sub-paths from the path value and let the middleware do its job. - ///
- ///
- /// Example: if user opens [...]/serilog-ui/other-route/my-log-id, - /// the FE would search for main.js into [...]/serilog-ui/other-route/main.js instead of [...]/serilog-ui/main.js. - ///
- private Task ChangeAssetRequestPath(HttpContext httpContext) + return CheckPath(path, "/(?!.*(assets/)).*") ? uiAppRoutes.GetHomeAsync() : _staticFileMiddleware.Invoke(httpContext); + } + + private StaticFileMiddleware CreateStaticFileMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory) + { + StaticFileOptions staticFileOptions = new() { - var from = $"{_options.RoutePrefix}/"; - const string to = "assets/"; + RequestPath = $"/{_options.RoutePrefix}", + FileProvider = new EmbeddedFileProvider(typeof(SerilogUiMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), + }; - var requestPath = httpContext.Request.GetEncodedUrl(); - var startOfWrongAssetSubPath = requestPath.IndexOf(from, StringComparison.Ordinal) + from.Length; - var endOfWrongAssetSubPath = requestPath.IndexOf(to, StringComparison.OrdinalIgnoreCase); - var pathToRemove = requestPath.Substring(startOfWrongAssetSubPath, endOfWrongAssetSubPath - startOfWrongAssetSubPath); + return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + } - var newPath = requestPath.Replace(pathToRemove, string.Empty); + private bool CheckPath(string currentPath, string onPath) + => Regex.IsMatch(currentPath, $"^/{Regex.Escape(_options.RoutePrefix)}{onPath}$", RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5)); + + /// + /// Since the FE app includes compiled js assets as relative urls, when the page has sub-paths the app tries to access the asset + /// from the whole sub-paths url. + ///
+ /// The StaticFileMiddleware always expose it from the serilog-ui route prefix. We remove the sub-paths from the path value and let the middleware do its job. + ///
+ ///
+ /// Example: if user opens [...]/serilog-ui/other-route/my-log-id, + /// the FE would search for main.js into [...]/serilog-ui/other-route/main.js instead of [...]/serilog-ui/main.js. + ///
+ private Task ChangeAssetRequestPath(HttpContext httpContext) + { + string from = $"{_options.RoutePrefix}/"; + const string to = "assets/"; - httpContext.Response.Redirect(newPath, true); + string requestPath = httpContext.Request.GetEncodedUrl(); + int startOfWrongAssetSubPath = requestPath.IndexOf(from, StringComparison.Ordinal) + from.Length; + int endOfWrongAssetSubPath = requestPath.IndexOf(to, StringComparison.OrdinalIgnoreCase); + string pathToRemove = requestPath.Substring(startOfWrongAssetSubPath, endOfWrongAssetSubPath - startOfWrongAssetSubPath); + string newPath = requestPath.Replace(pathToRemove, string.Empty); - return Task.CompletedTask; - } + httpContext.Response.Redirect(newPath, true); + + return Task.CompletedTask; } } \ No newline at end of file