Close admin trust audit gaps and stabilize live sweeps
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user