Compare commits

..
Author SHA1 Message Date
9aef10f281
make reinstall, better
All checks were successful
Dotnet build and test / build (pull_request) Successful in 12m56s
2026-09-21 14:58:40 +01:00
e28959b068
asciidoc => markdown 2026-09-21 14:39:25 +01:00
22c33c5388
Hide inserted estimates from client list; redirect anon to /signin
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 <noreply@anthropic.com>
2026-09-21 14:38:49 +01:00
7 changed files with 86 additions and 16 deletions

View file

@ -9,12 +9,12 @@ include .$(APP_INSTALL_ENV).env
reinstall: copy-binaries
@for project in $(APP_PROJECT_NAMES); do \
echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" \
echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" ; \
sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; done
@echo "💯 backend started"
reinstall-prod:
make APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production
make reinstall APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production
generated/$(APP_INSTALL_PREFIX)yavscOrg.service:
generated/$(APP_INSTALL_PREFIX)yavscBlogs.service:
@ -82,14 +82,16 @@ install: copy-binaries copy-services
@sudo systemctl daemon-reload
@for project in $(APP_PROJECT_NAMES); \
do \
sudo systemctl enable yavsc$${project} ; \
sudo systemctl start yavsc$${project} ; \
echo "🖒 enable $(APP_INSTALL_PREFIX)yavsc$${project}" ; \
sudo systemctl enable $(APP_INSTALL_PREFIX)yavsc$${project} ; \
echo "🚀 start $(APP_INSTALL_PREFIX)yavsc$${project}" ; \
sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; \
done
stop-services:
@for project in $(APP_PROJECT_NAMES); \
do \
echo "🔥 Stopping $(APP_INSTALL_PREFIX)yavsc$${project}" ; \
sudo systemctl stop $(APP_INSTALL_PREFIX)yavsc$${project} ; \
done

View file

@ -209,9 +209,12 @@ public sealed class EstimateApiControllerTests : IClassFixture<ApiWebServerFixtu
/// <summary>
/// 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 <paramref name="providerValidated"/> = false to seed a
/// freshly inserted request the provider hasn't worked on yet.
/// </summary>
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<ApplicationDbContext>();
@ -229,6 +232,7 @@ public sealed class EstimateApiControllerTests : IClassFixture<ApiWebServerFixtu
{
new() { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" },
},
ProviderValidationDate = providerValidated ? DateTime.UtcNow : default,
ClientValidationDate = clientValidated ? DateTime.UtcNow : default,
};
db.Estimates.Add(estimate);
@ -259,6 +263,26 @@ public sealed class EstimateApiControllerTests : IClassFixture<ApiWebServerFixtu
Assert.Equal("alice", item.GetProperty("ownerId").GetString());
}
[Fact]
public async Task GetOngoingEstimatesAsClient_excludes_inserted_requests_not_yet_provider_validated()
{
_fixture.ResetAndSeedActivityGraph();
var submittedId = SeedEstimate(clientId: "bob", clientValidated: false); // provider submitted: visible
SeedEstimate(clientId: "bob", clientValidated: false, providerValidated: false); // inserted, not worked on: excluded
using var http = NewClient("bob");
var response = await http.GetAsync("/api/v1/estimate/asclient", TestContext.Current.CancellationToken);
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
using var doc = JsonDocument.Parse(body);
var items = doc.RootElement.EnumerateArray().ToArray();
var item = Assert.Single(items);
Assert.Equal(submittedId, item.GetProperty("id").GetInt64());
}
[Fact]
public async Task GetOngoingEstimatesAsProvider_returns_only_ongoing_estimates_established_by_the_provider()
{

View file

@ -201,12 +201,18 @@ namespace Yavsc.Controllers
// GET: api/estimate/asclient
// Ongoing estimates (not yet validated by the client) where the
// current user is the client who initiated the request.
// An estimate only becomes visible to the client once the provider
// has actually established and submitted it (ProviderValidationDate);
// a freshly inserted request the provider hasn't worked on yet is
// not something the client can validate, so it's excluded.
[HttpGet("asclient")]
public IActionResult GetOngoingEstimatesAsClient()
{
var uid = User.GetUserId();
return Ok(_context.Estimates.Include(e => 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

View file

@ -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<TestWebApplicationFactory>
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);
}
}

View file

@ -215,6 +215,18 @@ public static class HostingExtensions
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
// 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

View file

@ -3,14 +3,14 @@
}
<markdown>
= La confidentialité
:hide-uri-scheme:
# La confidentialité
xref:Contact[Get Report]
À aucun moment, aucune adresse postale, aucune adresse e-mail ni aucun numéro de téléphone
ne sont transmis à personne. Seul le système et son link:Contact[possesseur] ont accès à ces informations.
De plus, le droit de retrait est permanent et sa mise en oeuvre link:/Account/Delete[immédiate].
À aucun moment, aucune adresse postale,
aucune adresse e-mail ni aucun numéro de téléphone
ne sont transmis à personne. Seul le système
et son link:Contact[possesseur] ont accès
à ces informations.
De plus, le droit de retrait est permanent
et sa [mise en oeuvre est immédiate](/Account/Delete).
</markdown>

View file

@ -113,6 +113,7 @@ public static class ServiceExtensions
options.ClaimsIssuer = authority;
options.Audience = audiences[0];
// Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs,
// Yavsc.Org itself) validates JWTs against the OP that runs
// on https://localhost:5001 with a self-signed dev cert.