Close admin trust audit gaps and stabilize live sweeps

This commit is contained in:
master
2026-03-12 10:14:00 +02:00
parent a00efb7ab2
commit 6964a046a5
50 changed files with 5968 additions and 2850 deletions

View File

@@ -26,6 +26,7 @@ public sealed class RouteDispatchMiddleware
// ReverseProxy paths that are legitimate browser navigation targets (e.g. OIDC flows)
// and must NOT be redirected to the SPA fallback.
private static readonly string[] BrowserProxyPaths = ["/connect", "/.well-known"];
private static readonly string[] SpaRoutesWithDocumentExtensions = ["/docs", "/docs/"];
public RouteDispatchMiddleware(
RequestDelegate next,
@@ -134,7 +135,7 @@ public sealed class RouteDispatchMiddleware
var spaFallback = route.Headers.TryGetValue("x-spa-fallback", out var spaValue) &&
string.Equals(spaValue, "true", StringComparison.OrdinalIgnoreCase);
if (spaFallback && !System.IO.Path.HasExtension(relativePath))
if (spaFallback && ShouldServeSpaFallback(relativePath))
{
var indexFile = fileProvider.GetFileInfo("/index.html");
if (indexFile.Exists && !indexFile.IsDirectory)
@@ -646,4 +647,22 @@ public sealed class RouteDispatchMiddleware
var accept = request.Headers.Accept.ToString();
return accept.Contains("text/html", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldServeSpaFallback(string relativePath)
{
if (!System.IO.Path.HasExtension(relativePath))
{
return true;
}
foreach (var prefix in SpaRoutesWithDocumentExtensions)
{
if (relativePath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}

View File

@@ -259,6 +259,59 @@ public sealed class RouteDispatchMiddlewareMicroserviceTests
}
}
[Fact]
public async Task InvokeAsync_StaticFilesRoute_DocsMarkdownPath_ServesSpaFallbackIndex()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"stella-router-docs-spa-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
await File.WriteAllTextAsync(
Path.Combine(tempDir, "index.html"),
"<!DOCTYPE html><html><body><h1>SPA Root</h1></body></html>");
var resolver = new StellaOpsRouteResolver(
[
new StellaOpsRoute
{
Type = StellaOpsRouteType.StaticFiles,
Path = "/",
TranslatesTo = tempDir,
Headers = new Dictionary<string, string> { ["x-spa-fallback"] = "true" }
}
]);
var httpClientFactory = new Mock<IHttpClientFactory>();
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
var middleware = new RouteDispatchMiddleware(
_ => Task.CompletedTask,
resolver,
httpClientFactory.Object,
NullLogger<RouteDispatchMiddleware>.Instance);
var context = new DefaultHttpContext();
context.Request.Method = HttpMethods.Get;
context.Request.Path = "/docs/modules/platform/architecture-overview.md";
context.Response.Body = new MemoryStream();
await middleware.InvokeAsync(context);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
context.Response.Body.Position = 0;
var body = await new StreamReader(context.Response.Body).ReadToEndAsync();
Assert.Contains("SPA Root", body, StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public async Task InvokeAsync_RegexCatchAll_CaptureGroupSubstitution_ResolvesServiceAndPath()
{