From 8cbc77eb1de6c294063c0dcb29fea3790ed5b7e9 Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Thu, 1 Jan 2026 17:38:48 +0100 Subject: [PATCH] Added swagger and elmah return-to-front-end button --- src/Server/Program.cs | 54 ++++++++++++++++++++++ src/Server/Views/Shared/_Layout.cshtml | 17 ++++++- src/Server/wwwroot/elmah-ui/custom.css | 54 ++++++++++++++++++++++ src/Server/wwwroot/elmah-ui/custom.js | 10 ++++ src/Server/wwwroot/swagger-ui/custom.css | 58 ++++++++++++++++++++++++ src/Server/wwwroot/swagger-ui/custom.js | 24 ++++++++++ 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/Server/wwwroot/elmah-ui/custom.css create mode 100644 src/Server/wwwroot/elmah-ui/custom.js create mode 100644 src/Server/wwwroot/swagger-ui/custom.css create mode 100644 src/Server/wwwroot/swagger-ui/custom.js 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 @@ + @if (User.IsInRole("Admin") || User.IsInRole("Swagger")) + { + + } + @if (User.IsInRole("Admin")) + { + + } 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 }); +});