Skip to content

Commit fa35875

Browse files
authored
Merge pull request #40 from sommmen/feature/inject-custom-css-and-js
feat(options): Home button url, custom css/js injection, allowing local requests
2 parents a8d3a94 + 8538b68 commit fa35875

File tree

11 files changed

+168
-18
lines changed

11 files changed

+168
-18
lines changed

README.md

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void ConfigureServices(IServiceCollection services)
3333
}
3434
```
3535

36-
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:
36+
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:
3737

3838
```csharp
3939
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@@ -58,13 +58,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
5858
}
5959
```
6060

61-
Default url to view log page is `http://<your-app>/serilog-ui`. If you want to change this url path, just config route prefix:
61+
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:
6262
```csharp
6363
app.UseSerilogUi(option => option.RoutePrefix = "logs");
6464
```
65+
6566
**Authorization configuration required**
6667

67-
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:
68+
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:
69+
6870
```csharp
6971
public void ConfigureServices(IServiceCollection services)
7072
{
@@ -80,10 +82,70 @@ public void ConfigureServices(IServiceCollection services)
8082
.
8183
.
8284
```
83-
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.
85+
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.
86+
87+
To disable access for local requests, (e.g. for testing authentication locally) set `AlwaysAllowLocalRequests` to `false`.
88+
89+
``` csharp
90+
services.AddSerilogUi(options => options
91+
.EnableAuthorization(authOption =>
92+
{
93+
authOption.AlwaysAllowLocalRequests = false;
94+
})
95+
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), "Logs"));
96+
```
97+
98+
## Limitations
99+
* Additional columns are not supported and only main columns can be retrieved.
100+
101+
## Options
102+
Options can be found in the [UIOptions](src/Serilog.Ui.Web/Extensions/UiOptions.cs) class.
103+
`internal` properties can generally be set via extension methods, see [SerilogUiOptionBuilderExtensions](src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs)
104+
105+
### Home url
106+
![image](https://user-images.githubusercontent.com/8641495/185874822-1d4b6f52-864c-4ffb-9064-6fc5ee9a079c.png)
107+
108+
The home button url can be customized by setting the `HomeUrl` property.
109+
110+
``` csharp
111+
app.UseSerilogUi(options =>
112+
{
113+
options.HomeUrl = "https://example.com/example?q=example";
114+
});
115+
```
116+
117+
### Custom Javascript and CSS
118+
119+
For customization of the dashboard UI custom JS and CSS can be injected.
120+
CSS gets injected in the `<head>` element. JS gets injected at the end of the `<body>` element by default.
121+
To inject JS in the `<head>` element set `injectInHead` to `true`.
122+
123+
``` csharp
124+
app.UseSerilogUi(x =>
125+
{
126+
x.InjectJavascript(path: "/js/serilog-ui/custom.js", injectInHead: false, type: "text/javascript");
127+
x.InjectStylesheet(path: "/css/serilog-ui/custom.css", media: "screen");
128+
});
129+
```
84130

85-
## Limitation
86-
* Additional columns are not supported and only main columns can be retrieved
131+
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).
132+
133+
``` csharp
134+
var builder = WebApplication.CreateBuilder(args);
135+
...
136+
app.UseStaticFiles();
137+
...
138+
```
139+
140+
With the default configuration static files are served under the wwwroot folder, so in the example above the file structure should be:
141+
![image](https://user-images.githubusercontent.com/8641495/185877921-99aaf19a-3e62-4ad9-85c3-47994e7e6ba1.png)
142+
143+
JS code can be ran when loading the file by wrapping the code in a function, and directly running that function like so:
144+
``` js
145+
(function () {
146+
console.log("custom.js is loaded.");
147+
})();
148+
```
87149

88150
## serilog-ui UI frontend development
89151

@@ -139,4 +201,4 @@ There are two Grunt tasks you can use to build the frontend project:
139201
- go to: chrome://settings/security => click Manage Certificates => go to Trusted Root Certification Authorities tab => import the .cer file previously exported
140202
- restart Chrome
141203
- 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."**)
142-
</details>
204+
</details>

samples/SampleWebApp/SampleWebApp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net5.0</TargetFramework>
4+
<TargetFramework>net6.0</TargetFramework>
55
<UserSecretsId>aspnet-SampleWebApp-93B544DE-DDCF-47C1-AD8A-BC87C4D6B954</UserSecretsId>
66
</PropertyGroup>
77

samples/SampleWebApp/Startup.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
7979

8080
app.UseAuthentication();
8181
app.UseAuthorization();
82-
app.UseSerilogUi();
82+
app.UseSerilogUi(x =>
83+
{
84+
x.RoutePrefix = "serilog-ui";
85+
x.HomeUrl = "/#Test";
86+
x.InjectJavascript("/js/serilog-ui/custom.js");
87+
});
8388

8489
app.UseEndpoints(endpoints =>
8590
{
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(function () {
2+
console.log("custom.js is loaded.");
3+
})();

src/Serilog.Ui.Web/Extensions/AuthorizationOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public class AuthorizationOptions
3030
/// </summary>
3131
/// <value> <c> true </c> if enabled; otherwise, <c> false </c>. </value>
3232
internal bool Enabled { get; set; } = false;
33+
34+
/// <summary>
35+
/// Whether to always allow local requests, defaults to <c>true</c>.
36+
/// </summary>
37+
/// <value> <c> true </c> if enabled; otherwise, <c> false </c>. </value>
38+
public bool AlwaysAllowLocalRequests { get; set; } = true;
3339
}
3440

3541
public enum AuthenticationType

src/Serilog.Ui.Web/Extensions/SerilogUiOptionBuilderExtensions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Serilog.Ui.Core;
33
using System;
4+
using System.Text;
45

56
namespace Serilog.Ui.Web
67
{
@@ -34,5 +35,40 @@ public static SerilogUiOptionsBuilder EnableAuthorization(this SerilogUiOptionsB
3435

3536
return optionsBuilder;
3637
}
38+
39+
/// <summary>
40+
/// Injects additional CSS stylesheets into the index.html page
41+
/// </summary>
42+
/// <param name="options"></param>
43+
/// <param name="path">A path to the stylesheet - i.e. the link "href" attribute</param>
44+
/// <param name="media">The target media - i.e. the link "media" attribute</param>
45+
/// <returns>The passed options object for chaining</returns>
46+
public static UiOptions InjectStylesheet(this UiOptions options, string path, string media = "screen")
47+
{
48+
var builder = new StringBuilder(options.HeadContent);
49+
builder.AppendLine($"<link href='{path}' rel='stylesheet' media='{media}' type='text/css' />");
50+
options.HeadContent = builder.ToString();
51+
return options;
52+
}
53+
54+
/// <summary>
55+
/// Injects additional Javascript files into the index.html page
56+
/// </summary>
57+
/// <param name="options"></param>
58+
/// <param name="path">A path to the javascript - i.e. the script "src" attribute</param>
59+
/// <param name="injectInHead">When true, injects the javascript in the &lt;head&gt; tag instead of the &lt;body&gt; tag</param>
60+
/// <param name="type">The script type - i.e. the script "type" attribute</param>
61+
/// <returns>The passed options object for chaining</returns>
62+
public static UiOptions InjectJavascript(this UiOptions options, string path, bool injectInHead = false, string type = "text/javascript")
63+
{
64+
var builder = new StringBuilder(injectInHead ? options.HeadContent : options.BodyContent);
65+
builder.AppendLine($"<script src='{path}' type='{type}'></script>");
66+
if(injectInHead)
67+
options.HeadContent = builder.ToString();
68+
else
69+
options.BodyContent = builder.ToString();
70+
return options;
71+
}
72+
3773
}
3874
}

src/Serilog.Ui.Web/Extensions/UiOptions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,28 @@ public class UiOptions
1414
/// <value> The route prefix. </value>
1515
public string RoutePrefix { get; set; } = "serilog-ui";
1616

17+
/// <summary>
18+
/// Gets or sets the URL for the home button
19+
/// </summary>
20+
/// <value> The URL for the home button. </value>
21+
public string HomeUrl { get; set; } = "/";
22+
1723
/// <summary>
1824
/// Gets or sets the type of the authentication.
1925
/// </summary>
2026
/// <value> The type of the authentication. </value>
2127
internal string AuthType { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the head content, a string that will be placed in the &lt;head&gt; of the index.html
31+
/// </summary>
32+
/// <value> The head content. </value>
33+
internal string HeadContent { get; set; } = string.Empty;
34+
35+
/// <summary>
36+
/// Gets or sets the head content, a string that will be placed in the &lt;body&gt; of the index.html
37+
/// </summary>
38+
/// <value> The head content. </value>
39+
internal string BodyContent { get; set; } = string.Empty;
2240
}
2341
}

src/Serilog.Ui.Web/SerilogUiMiddleware.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Newtonsoft.Json.Serialization;
1212
using Serilog.Ui.Core;
1313
using System;
14+
using System.Diagnostics;
1415
using System.IO;
1516
using System.Linq;
1617
using System.Net;
@@ -126,11 +127,16 @@ private async Task RespondWithIndexHtml(HttpResponse response)
126127
response.ContentType = "text/html;charset=utf-8";
127128

128129
await using var stream = IndexStream();
129-
var htmlBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync());
130-
var encodeAuthOpts = Uri.EscapeDataString(JsonConvert.SerializeObject(new { _options.RoutePrefix, _options.AuthType }, _jsonSerializerOptions));
131-
htmlBuilder.Replace("%(Configs)", encodeAuthOpts);
132-
133-
await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
130+
var htmlStringBuilder = new StringBuilder(await new StreamReader(stream).ReadToEndAsync());
131+
var encodeAuthOpts = Uri.EscapeDataString(JsonConvert.SerializeObject(new { _options.RoutePrefix, _options.AuthType, _options.HomeUrl }, _jsonSerializerOptions));
132+
133+
htmlStringBuilder
134+
.Replace("%(Configs)", encodeAuthOpts)
135+
.Replace("<meta name=\"dummy\" content=\"%(HeadContent)\">", _options.HeadContent)
136+
.Replace("<meta name=\"dummy\" content=\"%(BodyContent)\">", _options.BodyContent);
137+
138+
var htmlString = htmlStringBuilder.ToString();
139+
await response.WriteAsync(htmlString, Encoding.UTF8);
134140
}
135141

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

168174
private static bool CanAccess(HttpContext httpContext)
169175
{
170-
if (httpContext.Request.IsLocal())
176+
var authOptions = httpContext.RequestServices.GetService<AuthorizationOptions>();
177+
178+
if (httpContext.Request.IsLocal() && authOptions.AlwaysAllowLocalRequests)
171179
return true;
172180

173-
var authOptions = httpContext.RequestServices.GetService<AuthorizationOptions>();
174181
if (!authOptions.Enabled)
175182
return false;
176183

src/Serilog.Ui.Web/assets/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<link rel="stylesheet" type="text/css" href="~/node_modules/@fortawesome/fontawesome-free/css/solid.min.css" />
1010
<link rel="stylesheet" type="text/css" href="~/node_modules/bootstrap/dist/css/bootstrap.min.css" />
1111
<link rel="stylesheet" type="text/css" href="./css/main.css">
12+
<meta name="dummy" content="%(HeadContent)">
1213
</head>
1314
<body>
1415
<div class="wrapper d-flex align-items-stretch">
@@ -19,7 +20,7 @@ <h1>
1920
</h1>
2021
<ul class="list-unstyled components mb-5">
2122
<li class="active">
22-
<a href="/"><span class="fas fa-home"></span> Home</a>
23+
<a id="homeAnchor" href="/"><span class="fas fa-home"></span> Home</a>
2324
</li>
2425
</ul>
2526
<div class="footer">
@@ -215,5 +216,6 @@ <h5 class="modal-title" id="exampleModalLabel">Change Page</h5>
215216
window.config = JSON.parse('{"routePrefix":"serilog-ui","authType":"Jwt"}');
216217
}
217218
</script>
219+
<meta name="dummy" content="%(BodyContent)">
218220
</body>
219-
</html>
221+
</html>

src/Serilog.Ui.Web/assets/script/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,20 @@ const initListenersAndDynamicInfo = () => {
3535
document.querySelector('.custom-pagination-submit').addEventListener('click', changePageByModalChoice);
3636
}
3737

38+
const initHomeButton = () => {
39+
var homeButton = document.querySelector<HTMLAnchorElement>("#homeAnchor");
40+
41+
if (window?.config?.homeUrl && window.config.homeUrl != homeButton.href) {
42+
homeButton.href = window?.config?.homeUrl;
43+
}
44+
}
45+
3846
const init = () => {
3947
initListenersAndDynamicInfo();
4048
initTokenUi();
4149
fetchLogs();
50+
51+
initHomeButton();
4252
}
4353

4454
if (process.env.NODE_ENV === 'development') {

0 commit comments

Comments
 (0)