diff --git a/src/Server/Program.cs b/src/Server/Program.cs
index ccfcc4e..98c5ec3 100644
--- a/src/Server/Program.cs
+++ b/src/Server/Program.cs
@@ -15,6 +15,7 @@ using Microsoft.OpenApi.Models;
using Shared.Models;
using Microsoft.AspNetCore.ResponseCompression;
using System.Net;
+using System.Text;
var builder = WebApplication.CreateBuilder(args);
@@ -141,6 +142,57 @@ var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
+// Configure Elmah
+app.Use(async (context, next) =>
+{
+ if (context.Request.Path.StartsWithSegments("/elmah"))
+ {
+ context.Response.OnStarting(() =>
+ {
+ context.Response.Headers.Append(
+ "Content-Security-Policy",
+ "default-src 'self' 'unsafe-inline' 'unsafe-eval'"
+ );
+ return Task.CompletedTask;
+ });
+ }
+
+ await next();
+});
+app.Use(async (context, next) =>
+{
+ if (!context.Request.Path.StartsWithSegments("/elmah"))
+ {
+ await next();
+ return;
+ }
+
+ var originalBody = context.Response.Body;
+ using var memStream = new MemoryStream();
+ context.Response.Body = memStream;
+
+ await next();
+
+ memStream.Position = 0;
+ var html = await new StreamReader(memStream).ReadToEndAsync();
+
+ if (context.Response.ContentType?.Contains("text/html") == true)
+ {
+ html = html.Replace(
+ "",
+ """
+
+
+
+ """
+ );
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(html);
+ context.Response.ContentLength = bytes.Length;
+ await originalBody.WriteAsync(bytes);
+ context.Response.Body = originalBody;
+});
app.UseElmah();
app.MapHealthChecks("/healthz");
@@ -180,6 +232,8 @@ app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.EnablePersistAuthorization();
+ options.InjectStylesheet("/swagger-ui/custom.css");
+ options.InjectJavascript("/swagger-ui/custom.js");
});
//app.UseElmahExceptionPage(); // Messes with JSON response for API calls. Leaving this here so I don't accidentally put this in again later on.
diff --git a/src/Server/Views/Shared/_Layout.cshtml b/src/Server/Views/Shared/_Layout.cshtml
index 81ca9bb..734ed14 100644
--- a/src/Server/Views/Shared/_Layout.cshtml
+++ b/src/Server/Views/Shared/_Layout.cshtml
@@ -1,7 +1,10 @@
@using System.Globalization
@using Server.Services
+@using System.Net
@inject LocalizationService T
-
+@{
+ var currentUrl = WebUtility.HtmlEncode(Context.Request.Path);
+}
@@ -59,6 +62,18 @@
@T["Searchdomains"]
+ @if (User.IsInRole("Admin") || User.IsInRole("Swagger"))
+ {
+
+ @T["Swagger"]
+
+ }
+ @if (User.IsInRole("Admin"))
+ {
+
+ @T["Elmah"]
+
+ }
@T["Logout"]
diff --git a/src/Server/wwwroot/elmah-ui/custom.css b/src/Server/wwwroot/elmah-ui/custom.css
new file mode 100644
index 0000000..355b0b9
--- /dev/null
+++ b/src/Server/wwwroot/elmah-ui/custom.css
@@ -0,0 +1,54 @@
+.elmah-return-btn {
+ position: fixed;
+ top: 6px;
+ right: 24px;
+ z-index: 9999;
+
+ display: flex;
+ align-items: center;
+
+ height: 44px;
+ min-width: 44px;
+ padding: 0 14px;
+
+ background: #85ea2d;
+ color: black;
+ border-radius: 999px;
+ font-weight: 600;
+ text-decoration: none;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+
+ overflow: hidden;
+ white-space: nowrap;
+
+ justify-content: center;
+ text-decoration: none !important;
+
+ transition:
+ top 0.25s ease,
+ background-color 0.2s ease;
+}
+
+/* hidden label */
+.elmah-return-btn::before {
+ content: "Return to Front-end";
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ max-width: 0;
+ opacity: 0;
+ transition:
+ max-width 0.3s ease,
+ opacity 0.2s ease;
+}
+
+/* expand on hover */
+.elmah-return-btn:hover::before {
+ max-width: 220px;
+ padding: 0.5rem;
+ opacity: 1;
+}
+
+/* hover colors */
+.elmah-return-btn:hover {
+ background: #0b5ed7;
+ color: white;
+}
diff --git a/src/Server/wwwroot/elmah-ui/custom.js b/src/Server/wwwroot/elmah-ui/custom.js
new file mode 100644
index 0000000..b56fc10
--- /dev/null
+++ b/src/Server/wwwroot/elmah-ui/custom.js
@@ -0,0 +1,10 @@
+document.addEventListener('DOMContentLoaded', async () => {
+ const url = new URL(window.location.href);
+ const btn = document.createElement("a");
+ btn.href = url.searchParams.get('ReturnUrl') ?? "/";
+ btn.innerText = "⎋";
+ btn.setAttribute("aria-label", "Return to Front-End");
+ btn.className = "elmah-return-btn";
+
+ document.body.appendChild(btn);
+});
diff --git a/src/Server/wwwroot/swagger-ui/custom.css b/src/Server/wwwroot/swagger-ui/custom.css
new file mode 100644
index 0000000..c288f54
--- /dev/null
+++ b/src/Server/wwwroot/swagger-ui/custom.css
@@ -0,0 +1,58 @@
+.swagger-return-btn {
+ position: fixed;
+ top: 6px;
+ left: 24px;
+ z-index: 9999;
+
+ display: flex;
+ align-items: center;
+
+ height: 44px;
+ min-width: 44px;
+ padding: 0 14px;
+
+ background: #85ea2d;
+ color: black;
+ border-radius: 999px;
+ font-weight: 600;
+ text-decoration: none;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+
+ overflow: hidden;
+ white-space: nowrap;
+
+ justify-content: center;
+
+ transition:
+ top 0.25s ease,
+ background-color 0.2s ease;
+}
+
+/* hidden label */
+.swagger-return-btn::after {
+ content: "Return to Front-end";
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ max-width: 0;
+ opacity: 0;
+ transition:
+ max-width 0.3s ease,
+ opacity 0.2s ease;
+}
+
+/* expand on hover */
+.swagger-return-btn:hover::after {
+ max-width: 220px;
+ padding: 0.5rem;
+ opacity: 1;
+}
+
+/* hover colors */
+.swagger-return-btn:hover {
+ background: #0b5ed7;
+ color: white;
+}
+
+/* scrolled state */
+.swagger-return-btn.scrolled {
+ top: 24px;
+}
diff --git a/src/Server/wwwroot/swagger-ui/custom.js b/src/Server/wwwroot/swagger-ui/custom.js
new file mode 100644
index 0000000..4fcca92
--- /dev/null
+++ b/src/Server/wwwroot/swagger-ui/custom.js
@@ -0,0 +1,24 @@
+document.addEventListener('DOMContentLoaded', async () => {
+ const url = new URL(window.location.href);
+ const btn = document.createElement("a");
+ btn.href = url.searchParams.get('ReturnUrl') ?? "/";
+ btn.innerText = "⎋";
+ btn.setAttribute("aria-label", "Return to Front-End");
+ btn.className = "swagger-return-btn";
+
+ document.body.appendChild(btn);
+
+ const togglePosition = () => {
+ if (window.scrollY > 0) {
+ btn.classList.add("scrolled");
+ } else {
+ btn.classList.remove("scrolled");
+ }
+ };
+
+ // Initial state
+ togglePosition();
+
+ // On scroll
+ window.addEventListener("scroll", togglePosition, { passive: true });
+});