feat(api): serve SPA from wwwroot + SPA fallback for client-side routes
This commit is contained in:
@@ -157,6 +157,12 @@ using (var scope = app.Services.CreateScope())
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Serve the compiled SPA from wwwroot. Static files intentionally do NOT
|
||||
// require auth — the SPA itself sends users to /login when it sees a 401.
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.MapSessionEndpoints();
|
||||
app.MapNodeTypesEndpoints();
|
||||
app.MapConnectionProvidersEndpoints();
|
||||
@@ -166,6 +172,11 @@ app.MapExecutionsEndpoints();
|
||||
app.MapWebhookEndpoint(TimeSpan.FromSeconds(settings.WebhookDefaultTimeoutSeconds));
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new { ok = true }));
|
||||
|
||||
// SPA fallback — any GET that didn't match /api/* or /wh/* and doesn't map to
|
||||
// a static file gets index.html so client-side routes resolve.
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\**\*" CopyToOutputDirectory="Never" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
78
tests/VolcanicCore.Tests/StaticFilesFallbackTests.cs
Normal file
78
tests/VolcanicCore.Tests/StaticFilesFallbackTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class StaticFilesFallbackTests
|
||||
{
|
||||
// Every test writes its own tiny index.html into a per-test wwwroot dir so
|
||||
// we can assert the fallback served it verbatim.
|
||||
private sealed class WwwFactory : WebFactory
|
||||
{
|
||||
public string ContentRoot { get; } = Path.Combine(Path.GetTempPath(), $"vc-www-{Guid.NewGuid():N}");
|
||||
|
||||
public WwwFactory()
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(ContentRoot, "wwwroot"));
|
||||
File.WriteAllText(Path.Combine(ContentRoot, "wwwroot", "index.html"),
|
||||
"<!doctype html><title>SPA</title>");
|
||||
File.WriteAllText(Path.Combine(ContentRoot, "wwwroot", "static-thing.txt"),
|
||||
"hello static");
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
builder.UseContentRoot(ContentRoot);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing && Directory.Exists(ContentRoot))
|
||||
Directory.Delete(ContentRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Api_route_still_wins()
|
||||
{
|
||||
using var f = new WwwFactory();
|
||||
var c = f.CreateClient();
|
||||
var res = await c.GetAsync("/api/health");
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
var body = await res.Content.ReadAsStringAsync();
|
||||
Assert.Contains("\"ok\":true", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unknown_route_falls_back_to_index_html()
|
||||
{
|
||||
using var f = new WwwFactory();
|
||||
var c = f.CreateClient();
|
||||
var res = await c.GetAsync("/workflows/abc123");
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
Assert.Contains("SPA", await res.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Static_asset_is_served_directly()
|
||||
{
|
||||
using var f = new WwwFactory();
|
||||
var c = f.CreateClient();
|
||||
var res = await c.GetAsync("/static-thing.txt");
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
Assert.Equal("hello static", await res.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Webhook_route_still_wins_over_fallback()
|
||||
{
|
||||
using var f = new WwwFactory();
|
||||
var c = f.CreateClient();
|
||||
var res = await c.PostAsync("/wh/nonexistent/foo", new StringContent(""));
|
||||
// Router isn't registered → 404 from WebhookRouter, NOT the SPA index.
|
||||
Assert.Equal(HttpStatusCode.NotFound, res.StatusCode);
|
||||
Assert.DoesNotContain("SPA", await res.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user