From fa021f5f68a55c8f3bdede8b64c368e9cb11105c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81api=C5=84ski?= Date: Sat, 17 Oct 2026 22:50:27 +0200 Subject: [PATCH] fix: the panel at the root without a trailing slash Under a path base, the app's own address without the trailing slash (/app) has an empty request path, which Reiho cannot cut the route's "/" from: a signed-in user of the Polyglot demo got 500 at kododo.dev/polyglot/demo. The panel at the root now treats an empty path as "/". --- docs/architecture.md | 1 + src/Way.Shell.Tests/ShellPageTests.cs | 7 +++++++ src/Way.Shell/ShellEndpoints.cs | 10 +++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 3edd8f0..7f95e0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -214,6 +214,7 @@ Cel: potwierdzić, że podejście działa, zanim cokolwiek ruszymy w Way. - `ShellOptions.Notice`: notka nad każdym Way (panel i strona Way), np. „publiczne demo”. - `WayModule.IsHidden`: Way bez pozycji w menu panelu, otwierany pod `#/{id}/` (np. konto z linku pod nazwą użytkownika); panel przekierowuje na pierwszy widoczny Way. - `UseWayShell("/")`: panel w korzeniu aplikacji. Konwencja na grupie zawęża trasę catch-all Reiho do samego `/` (pliki panelu i tak idą z `AssetsPath`); bez tego każdy nieznany adres dostawał 404 panelu albo przekierowanie na logowanie (klient API z kluczem: 302 zamiast 404). Inne endpointy hosta odpowiadają jak dotąd. `MapWay` w korzeniu dalej zabronione. + - 2.1.1: pod PathBase adres aplikacji bez końcowego ukośnika (`/app`) ma pustą ścieżkę, z której Reiho nie umiał wyciąć `/` trasy panelu w korzeniu (500 u zalogowanego na demie Polyglota). Konwencja ustawia wtedy `Path = "/"`; test z adresem bez ukośnika. - Kontrakt modułu bez zmian (2). Testy: 53 × 3 TFM, npm 31; demo Shella (notka, ukryty Way „Account”) 40/40 × 4 przeglądarki. ### Faza 7: wydanie diff --git a/src/Way.Shell.Tests/ShellPageTests.cs b/src/Way.Shell.Tests/ShellPageTests.cs index 809636f..62bbb13 100644 --- a/src/Way.Shell.Tests/ShellPageTests.cs +++ b/src/Way.Shell.Tests/ShellPageTests.cs @@ -289,6 +289,13 @@ public async Task Panel_AtTheRoot_IsTheHomePage_NextToTheHostsEndpoints(string? Assert.Equal($"{prefix}/config/api", ShellApp.SettingsOf(html).GetProperty("ways")[0].GetProperty("apiBase").GetString()); Assert.Equal("sign in", await (await app.GetAsync($"{prefix}/login")).Content.ReadAsStringAsync()); Assert.Equal("single", ShellApp.SettingsOf(await (await app.GetAsync($"{prefix}/config/")).Content.ReadAsStringAsync()).GetProperty("mode").GetString()); + if (pathBase is not null) + { + // The app's own address without the trailing slash: under the path base the request path is empty. + var bare = await app.GetAsync(pathBase, user: "ada"); + Assert.Equal(HttpStatusCode.OK, bare.StatusCode); + Assert.Contains($"""""", await bare.Content.ReadAsStringAsync()); + } // Only the page: addresses the host does not map stay 404, not the panel's sign-in. Assert.Equal(HttpStatusCode.Unauthorized, (await app.GetAsync($"{prefix}/")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await app.GetAsync($"{prefix}/nothing")).StatusCode); diff --git a/src/Way.Shell/ShellEndpoints.cs b/src/Way.Shell/ShellEndpoints.cs index da4bd88..eb658ed 100644 --- a/src/Way.Shell/ShellEndpoints.cs +++ b/src/Way.Shell/ShellEndpoints.cs @@ -78,7 +78,15 @@ public RouteGroupBuilder UseWayShell(string path = "/admin") // answer every address the host does not map, with the panel's 404 or its sign-in instead of the host's. ((IEndpointConventionBuilder)group).Add(endpoint => { - if (endpoint is RouteEndpointBuilder route) route.RoutePattern = RoutePatternFactory.Parse("/"); + if (endpoint is not RouteEndpointBuilder { RequestDelegate: { } inner } route) return; + route.RoutePattern = RoutePatternFactory.Parse("/"); + // Under a path base the app's own address without the trailing slash (/app) has an empty path, which + // Reiho cannot cut the route's "/" from. + route.RequestDelegate = context => + { + if (!context.Request.Path.HasValue) context.Request.Path = "/"; + return inner(context); + }; }); return group; }