Adds a new JSON-bodied signature endpoint as a sibling of the
legacy PNG-based prosign/clisign routes. The legacy flow stays
intact: the TeX invoice templates (Bill_tex.cshtml,
Estimate_tex.cshtml) still consume the sign-{billingCode}-{id}.png
files the old endpoints write, and the new endpoint writes to a
distinct /signatures/ tree under UserFilesDirName. A future
migration commit will regenerate PNGs from the JSON payload and
decommission the PNG flow.
Scope
- New Signature entity (Yavsc.Server/Models/Billing/Signature.cs)
with FK to Estimate, FK to ApplicationUser (Signer), Type
(Pro/Client) enum, CoordinateMax (default 10_000), int[] Strokes
(native Npgsql mapping), CapturedAtUtc, FilePath. Multiple
versions per (EstimateId, Type) are allowed; the controller
reads the most recent.
- New Estimate.Signatures nav collection (InverseProperty) so the
composite index covers both sides of the relation.
- New DbSet<Signature> Signatures + composite index
(EstimateId, Type, CapturedAtUtc DESC) in ApplicationDbContext
OnModelCreating. DeleteBehavior.Cascade on Estimate deletion
cleans up signatures automatically.
- New EstimateSignatureFileHelper (Server/Helpers) with
ReceiveEstimateSignatureAsync(user, estimateId, type, payload).
Writes a yavsc.signature/v1 JSON envelope to
UserFilesDirName/{user}/signatures/sign-{type}-{estimateId}-{ticks}.json.
Quota update lives in the controller, not the helper, because
the helper has no DbContext access.
- New endpoint POST /api/bill/estimate/{id:long}/sign on
BillingController. Authz is body-driven (the bearer token is the
PostIt OAuth client, not the end user, so signerUserId is in
the JSON body, validated against Estimate.OwnerId/ClientId).
Returns 201 Created with the new Signature's metadata.
Plumbing
- SignatureSubmission (body type) lives next to BillingController
in the same file — small enough to keep colocated.
- The legacy prosign/clisign routes are untouched. They keep
the IFormFile PNG contract; the new endpoint is the JSON
counterpart.
Tests
- New EstimateSignatureFileHelperTests in Yavsc.Org.Tests
(8 tests, all green): filename format incl. lowercase type and
ticks, envelope v1 round-trip (parsed via JsonDocument, not
text matching), null payload rejected, non-positive
estimateId rejected. Disk side effects are isolated to a
per-test temp root via AbstractFileSystemHelpers.UserFilesDirName.
- Yavsc.Org.Tests full suite: 29/29 green.
- PostIt.Tests: 57/57 green (untouched by this commit).
- Builds: Yavsc.Server, Yavsc.Api, Yavsc.Org, Yavsc.Org.Tests
all compile clean.
Out of scope
- EF migration: the Signatures table doesn't exist in the
database yet. The migration is intentionally a separate
commit so the generated SQL can be reviewed against the
composite index and the int[] column type before it touches
any prod database. Until the migration lands, the new
endpoint will 500 on SaveChanges; the [DEV] button in
PostIt is the only call site, so this is acceptable.
- SignalR handler that opens the signature page on a
'devis received' push — commit 4.
Narrow ISmtpClient to the four operations MailSender actually uses,
behind a Yavsc.Interfaces.ISmtpClientFactory. Production wires
MailKitSmtpClient (SmtpClientFactory); tests wire a recording fake
(RecordingSmtpClientFactory). The fake is pre-registered in
WebServerFixture so SMTP calls are short-circuited; the EMailling
test now asserts the Connect -> Authenticate -> Send -> Disconnect
sequence. Yavsc.Abstract stays free of MailKit/MimeKit.
Move product-local package versions out of the root Directory.Packages.props
into per-product props files under src/<Product>/. The root file now only
contains versions for packages declared by two or more top-level products,
which is the actual shared set.
Each per-product Directory.Packages.props imports the root via
GetPathOfFileAbove so that the shared versions are inherited; this is
necessary because the .NET SDK picks the closest Directory.Packages.props
in the hierarchy and does not merge multiple ones.
Per-product file contents:
- src/cli/ Microsoft.AspNetCore.Razor.Language,
Microsoft.Extensions.{CommandLineUtils,Configuration,Hosting}
- src/PostIt/ Avalonia* and CommunityToolkit.Mvvm
- src/PostIt.Tests/ Avalonia.Headless{,XUnit}
- src/Yavsc.Org/ AsciiDocSharp*, Google.Apis.Compute.v1,
HigginsSoft.IdentityServer8.AspNetIdentity,
IdentityServer8.EntityFramework.Storage,
IdentityServer8.Security, IdentityServer8.Storage,
Microsoft.AspNetCore.Antiforgery, Authentication.Google,
Diagnostics.EntityFrameworkCore, Mvc.NewtonsoftJson,
SignalR, EntityFrameworkCore.Tools, Swashbuckle,
System.Security.Cryptography.Pkcs, YamlDotNet
- src/Yavsc.Org.Tests/ Microsoft.AspNetCore.Hosting,
Extensions.Caching.Memory, Options,
Options.ConfigurationExtensions,
Selenium.WebDriver, xunit.v3.{common,extensibility.core}
- src/Yavsc.Server/ Anthropic.SDK, Google.Apis.Calendar.v3,
Magick.NET-Q8-AnyCPU, MailKit, MimeKit,
Microsoft.AspNetCore.Http.Features, StaticFiles,
EntityFrameworkCore.SqlServer,
Npgsql.EntityFrameworkCore.PostgreSQL,
PayPalMerchantSDK, pazof.rules, RazorEngine.NetCore
- src/Yavsc.Web/ IdentityModel.AspNetCore
No per-product file is created for Yavsc.Api, Yavsc.Blogs, Yavsc.Abstract,
or templateWeb: Api and Blogs only declare the shared JwtBearer, Abstract
and templateWeb declare no package references at all.
Also includes a minor cosmetic update to FirstUIStript.cs (Firefox -> Chrome
driver, dedent, comment header). Tests previously failing on DataProtection
keyset / SMTP were unrelated environment issues (resolved by fixing the
SMTP password locally).
The Site:Audience setting was conflating two distinct concepts: an OAuth
JWT audience (a single resource identifier) and a CORS allow-list (an
array of origins). Collapsing them caused several latent bugs:
- OAuth/JWT validation expected a single string while CORS WithOrigins
accepts an array.
- Password-reset callback URLs and OAuth client RedirectUri/Origin were
being built from what was meant to be an audience identifier, not a
base URL.
- Yavsc.Org's main CORS policy was hardcoded to '*', with no way to
restrict it without code changes.
Changes:
- SiteSettings.Audience (string) replaced with CorsAllowedOrigins
(IList<string>).
- OAuth JWT Authority still reads Site:Authority; Audience now reads
Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false).
- MailSender and AccountController build reset-callback URLs from
Site:ExternalUrl.
- ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin
defaults on newly created clients.
- Yavsc.Api and Yavsc.Blogs now read CORS origins from
Site:CorsAllowedOrigins instead of hardcoded URLs.
Add shared AddYavscCors / AddYavscJwtBearer extension methods in
Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single
configuration contract across all runtime services (Api, Blogs, Org).
Fails closed when CorsAllowedOrigins is empty; fails fast at startup
when Site:Authority is missing.
Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers).
Local appsettings-*.json files (which carry deployment-specific values
and are gitignored) must be updated to add Site:CorsAllowedOrigins.
- Add in-memory database support for test isolation in WebServerFixture
- Implement TestMailSender fake SMTP provider for email test support
- Add thread synchronization to billing service registration to prevent race conditions
- Make RegisterBilling<T> idempotent to safely handle reconfiguration
- Configure test environment via in-memory settings (UseTestEmailSender, UseInMemoryDatabase)
- Add regression tests for billing module idempotency and duplicate registration detection
- Fix tests: EMaillingTests.SendEMailSynchrone, BillingServiceTests (2 tests), HaveConfigurationRoot (3 tests)
All core test infrastructure tests now passing.