diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/Login.razor b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/Login.razor index c27c9c9..2d29e0e 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/Login.razor +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/Login.razor @@ -6,13 +6,19 @@ cookie scheme's LoginPath="/login" redirect lands here for unauthenticated users. The card is the shared kit's : it renders a NATIVE static -
(username/password + hidden returnUrl). A native - form submit is not a Blazor event, so it reaches the minimal-API POST /login endpoint + (username/password + hidden returnUrl). A native + form submit is not a Blazor event, so it reaches the minimal-API POST /auth/login endpoint regardless of this app's InteractiveServer render mode. supplies the - token that PostLoginAsync's antiforgery.ValidateRequestAsync checks. *@ + token that PostLoginAsync's antiforgery.ValidateRequestAsync checks. + + NOTE: the POST target is /auth/login, NOT /login. This @page lives at "/login" and the + Razor Components endpoint matches ALL methods, so a POST to /login collided with the + minimal-API MapPost("/login") and threw AmbiguousMatchException (HTTP 500). Posting to a + distinct /auth/login path (mirroring ScadaBridge) keeps the GET page and POST handler from + sharing a route. *@ @attribute [AllowAnonymous] - + diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs index 69661b8..bf68cd1 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs @@ -29,8 +29,14 @@ public static class DashboardEndpointRouteBuilderExtensions // kit's . Its [AllowAnonymous] attribute overrides the // RequireAuthorization(ViewerPolicy) that MapRazorComponents() applies, // so the cookie scheme's LoginPath="/login" redirect resolves for anonymous users. + // + // The credential POST is mapped to /auth/login, NOT /login. The @page "/login" + // Razor Components endpoint matches ALL HTTP methods, so a MapPost("/login") shared + // the "/login" route with it and every POST threw AmbiguousMatchException (HTTP 500). + // A distinct /auth/login path (as ScadaBridge does) keeps the GET page and the POST + // handler on separate routes. The form posts here. endpoints.MapPost( - "/login", + "/auth/login", (HttpContext httpContext, IAntiforgery antiforgery, IDashboardAuthenticator authenticator) => PostLoginAsync(httpContext, antiforgery, authenticator)) .AllowAnonymous() diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs index 98ee67e..2eb1667 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs @@ -87,13 +87,18 @@ public sealed class GatewayApplicationTests Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/events"); Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/settings"); - // GET /login is now served by the [AllowAnonymous] Blazor component + // GET /login is served by the [AllowAnonymous] Blazor component // (Components/Pages/Login.razor → @page "/login"), not a named minimal-API - // endpoint. The form still POSTs to the minimal-API DashboardLoginPost endpoint. + // endpoint. The credential POST goes to the DashboardLoginPost endpoint at + // /auth/login — a DISTINCT route. The Blazor component endpoint matches all HTTP + // methods, so sharing the "/login" route with MapPost previously made POST /login + // ambiguous (AmbiguousMatchException → HTTP 500). Pinning the POST to /auth/login + // is the regression guard for that fix. Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/login" && endpoint.Metadata.GetMetadata() is not null); Assert.Contains(endpoints, endpoint => - endpoint.Metadata.GetMetadata()?.EndpointName == "DashboardLoginPost"); + endpoint.Metadata.GetMetadata()?.EndpointName == "DashboardLoginPost" + && endpoint.RoutePattern.RawText == "/auth/login"); Assert.Contains(endpoints, endpoint => endpoint.Metadata.GetMetadata()?.EndpointName == "DashboardLogout"); }