Fix proxied live updates

This commit is contained in:
2026-05-05 01:55:59 +02:00
parent 2be1fc599a
commit b8bd92e3dc
3 changed files with 82 additions and 1 deletions

View File

@@ -200,6 +200,55 @@ bash ./scripts/deploy.sh
The script publishes the app locally, uploads a release to `/root/docker/rpgroller/releases/<UTC timestamp>`, updates `/root/docker/rpgroller/current`, rebuilds the `rpgroller` image, and recreates the `rpgroller` container. The SQLite database is preserved because the container keeps using the existing bind mount at `/root/docker/rpgroller/data`. The script publishes the app locally, uploads a release to `/root/docker/rpgroller/releases/<UTC timestamp>`, updates `/root/docker/rpgroller/current`, rebuilds the `rpgroller` image, and recreates the `rpgroller` container. The SQLite database is preserved because the container keeps using the existing bind mount at `/root/docker/rpgroller/data`.
Reverse proxy requirements for production:
- Use `rpgroller.franktovar.de` as the only canonical host.
- Forward `X-Forwarded-For` and `X-Forwarded-Proto` so ASP.NET Core can mark the session cookie as secure behind TLS termination.
- Proxy `/_blazor` with WebSocket upgrade headers.
- Proxy `/api/events/state` as Server-Sent Events with buffering disabled, for example:
```nginx
server {
server_name rpgroller.franktovar.de;
location /_blazor {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
}
location /api/events/state {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 3600;
add_header X-Accel-Buffering no;
}
location / {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300;
}
}
```
Environment overrides: Environment overrides:
- Set `ConnectionStrings__RpgRoller` to point at a custom SQLite database. - Set `ConnectionStrings__RpgRoller` to point at a custom SQLite database.

View File

@@ -12,7 +12,8 @@ public sealed class AuthApiTests(WebApplicationFactory<Program> factory) : ApiTe
Assert.Equal("alice", registerResult.Username); Assert.Equal("alice", registerResult.Username);
Assert.Contains(registerResult.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase)); Assert.Contains(registerResult.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase));
var duplicate = await client.PostAsJsonAsync("/api/auth/register", new RegisterRequest("alice", "Password123", "Alice 2")); var duplicate = await client.PostAsJsonAsync("/api/auth/register",
new RegisterRequest("alice", "Password123", "Alice 2"));
Assert.Equal(HttpStatusCode.BadRequest, duplicate.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, duplicate.StatusCode);
var loginResult = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest("alice", "Password123")); var loginResult = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest("alice", "Password123"));
@@ -44,4 +45,27 @@ public sealed class AuthApiTests(WebApplicationFactory<Program> factory) : ApiTe
var usernames = await GetAsync<IReadOnlyList<string>>(client, "/api/users/usernames"); var usernames = await GetAsync<IReadOnlyList<string>>(client, "/api/users/usernames");
Assert.Equal(["amy", "bob", "zoe"], usernames); Assert.Equal(["amy", "bob", "zoe"], usernames);
} }
[Fact]
public async Task LoginCookie_IsMarkedSecure_WhenForwardedProtoIsHttps()
{
using var factory = CreateFactory();
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(client, "proxy-user", "Password123", "Proxy User");
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login")
{
Content = JsonContent.Create(new LoginRequest("proxy-user", "Password123"))
};
request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", "https");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value);
var setCookie = Assert.Single(response.Headers.GetValues("Set-Cookie"));
Assert.Contains("rpgroller_session=", setCookie);
Assert.Contains("secure", setCookie, StringComparison.OrdinalIgnoreCase);
}
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.HttpOverrides;
using RpgRoller.Api; using RpgRoller.Api;
using RpgRoller.Components; using RpgRoller.Components;
using RpgRoller.Contracts; using RpgRoller.Contracts;
@@ -7,6 +8,12 @@ using RpgRoller.Hosting;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment); builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.AddResponseCompression(options => builder.Services.AddResponseCompression(options =>
{ {
options.EnableForHttps = true; options.EnableForHttps = true;
@@ -18,6 +25,7 @@ builder.Services.AddScoped<WorkspaceQueryService>();
var app = builder.Build(); var app = builder.Build();
app.InitializeRpgRollerState(); app.InitializeRpgRollerState();
app.UseForwardedHeaders();
var configuredPathBase = builder.Configuration["PathBase"]; var configuredPathBase = builder.Configuration["PathBase"];
if (!string.IsNullOrWhiteSpace(configuredPathBase)) if (!string.IsNullOrWhiteSpace(configuredPathBase))