From caea7463b1ab0fc8b7dbad7acbf05c190717452e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 21 Sep 2026 14:38:49 +0100 Subject: [PATCH] Hide inserted estimates from client list; redirect anon to /signin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Estimates with status Inserted (provider hasn't worked on them yet) were showing up in the client's "Mes devis à valider" list. The asclient endpoint now requires ProviderValidationDate to be set, so only estimates the provider has actually established and submitted are visible to the client. The asprovider endpoint is unchanged (providers still need to see incoming unprocessed requests). Also fix the anonymous-access redirect on Yavsc.Org: the application cookie's LoginPath defaulted to /Account/Login, which has no controller. Anonymous hits to a restricted page now redirect to the real /signin endpoint (with /signout and /Account/AccessDenied set to match the existing controller routes). Co-Authored-By: Claude Code --- .../EstimateApiControllerTests.cs | 28 +++++++++++++++++-- .../Business/EstimateApiController.cs | 8 +++++- .../Smoke/AccountSmokeTests.cs | 25 +++++++++++++++++ src/Yavsc.Org/Extensions/HostingExtensions.cs | 12 ++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs b/src/Yavsc.Api.Test/EstimateApiControllerTests.cs index cb945f51a..d8cae0265 100644 --- a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs +++ b/src/Yavsc.Api.Test/EstimateApiControllerTests.cs @@ -209,9 +209,12 @@ public sealed class EstimateApiControllerTests : IClassFixture /// Seeds an estimate owned by alice (the provider) for the given - /// client, optionally already validated by the client. + /// client, optionally already validated by the client. By default the + /// estimate is also provider-validated (i.e. the provider has submitted + /// it); pass = false to seed a + /// freshly inserted request the provider hasn't worked on yet. /// - private long SeedEstimate(string clientId, bool clientValidated) + private long SeedEstimate(string clientId, bool clientValidated, bool providerValidated = true) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -229,6 +232,7 @@ public sealed class EstimateApiControllerTests : IClassFixture e.Bill) - .Where(e => e.ClientId == uid && e.ClientValidationDate == default)); + .Where(e => e.ClientId == uid + && e.ProviderValidationDate != default + && e.ClientValidationDate == default)); } // GET: api/estimate/asprovider diff --git a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs index 61d774c57..5e7ab7776 100644 --- a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs +++ b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs @@ -1,4 +1,7 @@ +using System.Net; +using System.Net.Http; using IdentityServer8.Stores; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -40,4 +43,26 @@ public class AccountSmokeTests : IClassFixture Assert.Null(exception); } + + // Regression: anonymous access to a restricted page used to redirect to + // the ASP.NET Identity default LoginPath "/Account/Login", which has no + // controller here. The application cookie must redirect to the real + // sign-in endpoint at /signin instead. + [Fact] + public async Task Anonymous_access_to_protected_page_redirects_to_signin() + { + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + }); + + // EstimateController is [Authorize]; GET /Estimate hits Index(). + var response = await client.GetAsync("/Estimate", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + var location = response.Headers.Location; + Assert.NotNull(location); + Assert.Equal("/signin", location.AbsolutePath); + Assert.Contains("ReturnUrl=%2FEstimate", location.Query); + } } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 8c1db2a73..ea035001c 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -215,6 +215,18 @@ public static class HostingExtensions services.AddScoped, UserClaimsPrincipalFactory>(); + // The default application cookie LoginPath is "/Account/Login", which + // has no controller here — anonymous access to a restricted page must + // redirect to the actual sign-in endpoint instead. The paths mirror + // the route attributes on AccountController (Constants.SigninPath etc., + // which carry a "~/" prefix that the cookie middleware does not want). + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/signin"; + options.LogoutPath = "/signout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + }); + // Dev-only: Chromium rejects SameSite=None without Secure on http:// // (e.g. http://localhost:5000). The default Identity cookie policy // sets SameSite=None, which is invalid without Secure. Force Lax in