Skip to content

Commit

Permalink
Merge pull request #40 from sommmen/feature/inject-custom-css-and-js
Browse files Browse the repository at this point in the history
feat(options): Home button url, custom css/js injection, allowing local requests
  • Loading branch information
mo-esmp authored Aug 23, 2022
2 parents a8d3a94 + 8538b68 commit fa35875
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 18 deletions.
76 changes: 69 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void ConfigureServices(IServiceCollection services)
}
```

In the `Startup.Configure` method, enable the middleware for serving logs UI. Place a call to the `UseSerilogUi` middleware after authentication and authorization middlewares otherwise authentication may not work for you:
In the `Startup.Configure` method, enable the middleware for serving the log UI. Place a call to the `UseSerilogUi` middleware after authentication and authorization middlewares, otherwise authentication may not work for you:

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand All @@ -58,13 +58,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
}
```

Default url to view log page is `http://<your-app>/serilog-ui`. If you want to change this url path, just config route prefix:
The default url to view the log page is `http://<your-app>/serilog-ui`. If you want to change this url path, just configure the route prefix:
```csharp
app.UseSerilogUi(option => option.RoutePrefix = "logs");
```

**Authorization configuration required**

By default serilog-ui allows access to log page only for local requests. In order to give appropriate rights for production use, you need to configuring authorization. You can secure log page by allwoing specific users or roles to view logs:
By default serilog-ui allows access to the log page only for local requests. In order to give appropriate rights for production use, you need to configure authorization. You can secure the log page by allowing specific users or roles to view logs:

```csharp
public void ConfigureServices(IServiceCollection services)
{
Expand All @@ -80,10 +82,70 @@ public void ConfigureServices(IServiceCollection services)
.
.
```
Only `User1` and `User2` or users with `AdminRole` role can view logs. If you set `AuthenticationType` to `Jwt`, you can set jwt token and `Authorization` header will be added to the request and for `Cookie` just login into you website and no extra step is required.
Only `User1` and `User2` or users with `AdminRole` role can view logs. If you set `AuthenticationType` to `Jwt`, you can set a jwt token and an `Authorization` header will be added to the request and for `Cookie` just login into you website and no extra step is required.

To disable access for local requests, (e.g. for testing authentication locally) set `AlwaysAllowLocalRequests` to `false`.

``` csharp
services.AddSerilogUi(options => options
.EnableAuthorization(authOption =>
{
authOption.AlwaysAllowLocalRequests = false;
})
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), "Logs"));
```

## Limitations
* Additional columns are not supported and only main columns can be retrieved.

## Options
Options can be found in the [UIOptions](src/Serilog.Ui.Web/Extensions/UiOptions.cs) class.
`internal` properties can generally be set via extension methods, see [SerilogUiOptionBuilderExtensions](src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs)

### Home url
![image](https://user-images.githubusercontent.com/8641495/185874822-1d4b6f52-864c-4ffb-9064-6fc5ee9a079c.png)
The home button url can be customized by setting the `HomeUrl` property.

``` csharp
app.UseSerilogUi(options =>
{
options.HomeUrl = "https://example.com/example?q=example";
});
```

### Custom Javascript and CSS

For customization of the dashboard UI custom JS and CSS can be injected.
CSS gets injected in the `<head>` element. JS gets injected at the end of the `<body>` element by default.
To inject JS in the `<head>` element set `injectInHead` to `true`.

``` csharp
app.UseSerilogUi(x =>
{
x.InjectJavascript(path: "/js/serilog-ui/custom.js", injectInHead: false, type: "text/javascript");
x.InjectStylesheet(path: "/css/serilog-ui/custom.css", media: "screen");
});
```

## Limitation
* Additional columns are not supported and only main columns can be retrieved
Custom JS/CSS files must be served by the backend via [static file middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/static-files).
``` csharp
var builder = WebApplication.CreateBuilder(args);
...
app.UseStaticFiles();
...
```

With the default configuration static files are served under the wwwroot folder, so in the example above the file structure should be:
![image](https://user-images.githubusercontent.com/8641495/185877921-99aaf19a-3e62-4ad9-85c3-47994e7e6ba1.png)
JS code can be ran when loading the file by wrapping the code in a function, and directly running that function like so:
``` js
(function () {
console.log("custom.js is loaded.");
})();
```

## serilog-ui UI frontend development

Expand Down Expand Up @@ -139,4 +201,4 @@ There are two Grunt tasks you can use to build the frontend project:
- go to: chrome://settings/security => click Manage Certificates => go to Trusted Root Certification Authorities tab => import the .cer file previously exported
- restart Chrome
- you should be able to run the dev environment on both localhost and 127.0.0.1 (to check if it's working fine, open the console: you'll find a red message: **"[MSW] Mocking enabled."**)
</details>
</details>
2 changes: 1 addition & 1 deletion samples/SampleWebApp/SampleWebApp.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>aspnet-SampleWebApp-93B544DE-DDCF-47C1-AD8A-BC87C4D6B954</UserSecretsId>
</PropertyGroup>

Expand Down
7 changes: 6 additions & 1 deletion samples/SampleWebApp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

app.UseAuthentication();
app.UseAuthorization();
app.UseSerilogUi();
app.UseSerilogUi(x =>
{
x.RoutePrefix = "serilog-ui";
x.HomeUrl = "/#Test";
x.InjectJavascript("/js/serilog-ui/custom.js");
});

app.UseEndpoints(endpoints =>
{
Expand Down
3 changes: 3 additions & 0 deletions samples/SampleWebApp/wwwroot/js/serilog-ui/custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(function () {
console.log("custom.js is loaded.");
})();
6 changes: 6 additions & 0 deletions src/Serilog.Ui.Web/Extensions/AuthorizationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public class AuthorizationOptions
/// </summary>
/// <value> <c> true </c> if enabled; otherwise, <c> false </c>. </value>
internal bool Enabled { get; set; } = false;

/// <summary>
/// Whether to always allow local requests, defaults to <c>true</c>.
/// </summary>
/// <value> <c> true </c> if enabled; otherwise, <c> false </c>. </value>
public bool AlwaysAllowLocalRequests { get; set; } = true;
}

public enum AuthenticationType
Expand Down
36 changes: 36 additions & 0 deletions src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog.Ui.Core;
using System;
using System.Text;

namespace Serilog.Ui.Web
{
Expand Down Expand Up @@ -34,5 +35,40 @@ public static SerilogUiOptionsBuilder EnableAuthorization(this SerilogUiOptionsB

return optionsBuilder;
}

/// <summary>
/// Injects additional CSS stylesheets into the index.html page
/// </summary>
/// <param name="options"></param>
/// <param name="path">A path to the stylesheet - i.e. the link "href" attribute</param>
/// <param name="media">The target media - i.e. the link "media" attribute</param>
/// <returns>The passed options object for chaining</returns>
public static UiOptions InjectStylesheet(this UiOptions options, string path, string media = "screen")
{
var builder = new StringBuilder(options.HeadContent);
builder.AppendLine($"<link href='{path}' rel='stylesheet' media='{media}' type='text/css' />");
options.HeadContent = builder.ToString();
return options;
}

/// <summary>
/// Injects additional Javascript files into the index.html page
/// </summary>
/// <param name="options"></param>
/// <param name="path">A path to the javascript - i.e. the script "src" attribute</param>
/// <param name="injectInHead">When true, injects the javascript in the &lt;head&gt; tag instead of the &lt;body&gt; tag</param>
/// <param name="type">The script type - i.e. the script "type" attribute</param>
/// <returns>The passed options object for chaining</returns>
public static UiOptions InjectJavascript(this UiOptions options, string path, bool injectInHead = false, string type = "text/javascript")
{
var builder = new StringBuilder(injectInHead ? options.HeadContent : options.BodyContent);
builder.AppendLine($"<script src='{path}' type='{type}'></script>");
if(injectInHead)
options.HeadContent = builder.ToString();
else
options.BodyContent = builder.ToString();
return options;
}

}
}
18 changes: 18 additions & 0 deletions src/Serilog.Ui.Web/Extensions/UiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,28 @@ public class UiOptions
/// <value> The route prefix. </value>
public string RoutePrefix { get; set; } = "serilog-ui";

/// <summary>
/// Gets or sets the URL for the home button
/// </summary>
/// <value> The URL for the home button. </value>
public string HomeUrl { get; set; } = "/";

/// <summary>
/// Gets or sets the type of the authentication.
/// </summary>
/// <value> The type of the authentication. </value>
internal string AuthType { get; set; }

/// <summary>
/// Gets or sets the head content, a string that will be placed in the &lt;head&gt; of the index.html
/// </summary>
/// <value> The head content. </value>
internal string HeadContent { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the head content, a string that will be placed in the &lt;body&gt; of the index.html
/// </summary>
/// <value> The head content. </value>
internal string BodyContent { get; set; } = string.Empty;
}
}
21 changes: 14 additions & 7 deletions src/Serilog.Ui.Web/SerilogUiMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Newtonsoft.Json.Serialization;
using Serilog.Ui.Core;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -126,11 +127,16 @@ private async Task RespondWithIndexHtml(HttpResponse response)
response.ContentType = "text/html;charset=utf-8";

await using var stream = IndexStream();
var htmlBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync());
var encodeAuthOpts = Uri.EscapeDataString(JsonConvert.SerializeObject(new { _options.RoutePrefix, _options.AuthType }, _jsonSerializerOptions));
htmlBuilder.Replace("%(Configs)", encodeAuthOpts);

await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
var htmlStringBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync());
var encodeAuthOpts = Uri.EscapeDataString(JsonConvert.SerializeObject(new { _options.RoutePrefix, _options.AuthType, _options.HomeUrl }, _jsonSerializerOptions));

htmlStringBuilder
.Replace("%(Configs)", encodeAuthOpts)
.Replace("<meta name=\"dummy\" content=\"%(HeadContent)\">", _options.HeadContent)
.Replace("<meta name=\"dummy\" content=\"%(BodyContent)\">", _options.BodyContent);

var htmlString = htmlStringBuilder.ToString();
await response.WriteAsync(htmlString, Encoding.UTF8);
}

private Func<Stream> IndexStream { get; } = () => typeof(AuthorizationOptions).GetTypeInfo().Assembly
Expand Down Expand Up @@ -167,10 +173,11 @@ private async Task<string> FetchLogsAsync(HttpContext httpContext)

private static bool CanAccess(HttpContext httpContext)
{
if (httpContext.Request.IsLocal())
var authOptions = httpContext.RequestServices.GetService<AuthorizationOptions>();

if (httpContext.Request.IsLocal() && authOptions.AlwaysAllowLocalRequests)
return true;

var authOptions = httpContext.RequestServices.GetService<AuthorizationOptions>();
if (!authOptions.Enabled)
return false;

Expand Down
6 changes: 4 additions & 2 deletions src/Serilog.Ui.Web/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="stylesheet" type="text/css" href="~/node_modules/@fortawesome/fontawesome-free/css/solid.min.css" />
<link rel="stylesheet" type="text/css" href="~/node_modules/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="./css/main.css">
<meta name="dummy" content="%(HeadContent)">
</head>
<body>
<div class="wrapper d-flex align-items-stretch">
Expand All @@ -19,7 +20,7 @@ <h1>
</h1>
<ul class="list-unstyled components mb-5">
<li class="active">
<a href="/"><span class="fas fa-home"></span> Home</a>
<a id="homeAnchor" href="/"><span class="fas fa-home"></span> Home</a>
</li>
</ul>
<div class="footer">
Expand Down Expand Up @@ -215,5 +216,6 @@ <h5 class="modal-title" id="exampleModalLabel">Change Page</h5>
window.config = JSON.parse('{"routePrefix":"serilog-ui","authType":"Jwt"}');
}
</script>
<meta name="dummy" content="%(BodyContent)">
</body>
</html>
</html>
10 changes: 10 additions & 0 deletions src/Serilog.Ui.Web/assets/script/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ const initListenersAndDynamicInfo = () => {
document.querySelector('.custom-pagination-submit').addEventListener('click', changePageByModalChoice);
}

const initHomeButton = () => {
var homeButton = document.querySelector<HTMLAnchorElement>("#homeAnchor");

if (window?.config?.homeUrl && window.config.homeUrl != homeButton.href) {
homeButton.href = window?.config?.homeUrl;
}
}

const init = () => {
initListenersAndDynamicInfo();
initTokenUi();
fetchLogs();

initHomeButton();
}

if (process.env.NODE_ENV === 'development') {
Expand Down
1 change: 1 addition & 0 deletions src/Serilog.Ui.Web/assets/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
config: {
authType?: string,
routePrefix?: string,
homeUrl?: string
}
}
export interface globalThis {
Expand Down

1 comment on commit fa35875

@sommmen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mo-esmp hiya - could you make a release for me so i can start using this?

Please sign in to comment.