Commit graph

3,251 commits

Author SHA1 Message Date
d04a05a5f2 PostIt: fix blog API double-prefix; drop redundant New
BlogApiClient's "api/blog" path combined with the BaseAddress's
"api/v1/" prefix to produce 404s on every call. Drop the redundant
"api/" segment, let Save handle the create case (no selection) and
remove the now-redundant New button + command.
2026-07-06 20:58:58 +01:00
9921d0df5f clean up 2026-07-06 19:19:28 +01:00
fcb93d41d2 code cleanup 2026-07-06 03:31:05 +01:00
694ea7ff23 build 2026-07-06 03:26:57 +01:00
ed9354b0f5 revert 2026-07-06 03:25:19 +01:00
9b15716e31 migration 2026-07-06 03:17:23 +01:00
d915bca1ff Merge branch 'feat/estimate' 2026-07-06 01:07:43 +01:00
a7ca2eb53a could fix the CI 2026-07-06 01:06:50 +01:00
b05be22da7 Merge pull request #66 from pazof/feat/estimate
Feat/estimate
2026-07-06 00:58:04 +01:00
2e886eb361 Potential fix for pull request finding 'CodeQL / Missing cross-site request forgery token validation'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-06 00:57:47 +01:00
6f06fdbd35 Merge pull request #65 from pazof/dependabot/github_actions/all-actions-640176b5ab
build(deps): bump actions/checkout from 4 to 7 in the all-actions group across 1 directory
2026-07-06 00:50:17 +01:00
a523d4071e tests 2026-07-06 00:47:35 +01:00
470a887588 fixes the startup 2026-07-06 00:14:22 +01:00
135003a913 Identity reloaded 2026-07-05 23:56:10 +01:00
dependabot[bot]
9e0e89cddf build(deps): bump actions/checkout
Bumps the all-actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-05 16:04:33 +00:00
8f8ee6383f refacto FrontmatterParser 2026-07-04 22:58:29 +01:00
5e4d548708 refacto query status 2026-07-04 22:35:53 +01:00
f5a9a3ddef renaming Reviewed 2026-07-04 22:25:59 +01:00
40897f4f83 layouts 2026-07-04 20:09:58 +01:00
c1b7170143 titres 2026-07-04 20:03:56 +01:00
1a50b8a205 gives titles 2026-07-04 19:57:03 +01:00
8ebb2f49d4 Ui fixes 2026-07-04 19:49:48 +01:00
1e5333fdd2 fixes 2026-07-04 18:46:24 +01:00
cdbc173630 Revert "search all user by email at forgotten password"
This reverts commit b0e48587cd.
2026-07-04 17:29:49 +01:00
b0e48587cd search all user by email at forgotten password 2026-07-04 17:24:12 +01:00
6ef731958e Activity moderated 2026-07-04 17:11:43 +01:00
bd51df6ed9 drop the deigner.cs 2026-07-04 16:06:02 +01:00
Lum
9c58523dc6 feat(api): POST /api/bill/estimate/{id}/sign — JSON signature capture
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.
2026-07-04 15:47:55 +01:00
2759661aa5 refacto chathub 2026-07-04 15:28:07 +01:00
Lum
7397f54ea2 feat(postit): signature capture page (dev entry, file persistence)
Builds on f9cfd560 (SignaturePadControl + SignaturePadData) with a
full Avalonia page that captures signatures, renders them as Polylines,
and persists the wire-format payload to ~/.local/share/PostIt/signatures
as JSON v1.

Scope
- New SignaturePage (axaml + code-behind) hosts the render-agnostic
  control: a fixed-size Border is the hit-test surface, an overlaid
  Canvas is rebuilt on every RedrawRequested from the Strokes buffer.
- SignaturePageViewModel wraps the control: exposes StrokeCount /
  PointCount / StatusMessage, Clear and CaptureAsync commands, and
  Attach/Detach for view-lifetime ownership.
- CaptureAsync writes a JSON envelope { format, coordinateMax,
  capturedAtUtc, strokes, strokeCount } to
  LocalApplicationData/PostIt/signatures/signature-yyyyMMdd-HHmmssfff.json.
  This is a stop-gap; the production transport will be
  POST /api/signature/{devisId} on Yavsc.Api (commit 3+).
- Entry point is a [DEV] button on MainPage that pushes the page
  onto the NavigationPage. The production trigger is a SignalR push
  from Yavsc.Org ("devis received, sign here") landing on a hub
  handler — the button and its Click handler are explicitly marked
  dev-only and tracked for removal in the same commit that wires
  the SignalR handler.

Plumbing
- App.axaml.cs: SignaturePage and SignaturePageViewModel registered
  as Transient in the DI container.
- ViewLocator: routes SignaturePageViewModel to SignaturePage.
- SignaturePadData: adds PointCount (sum of pairs across strokes),
  used by the VM status bar and the test surface.

Tests (57/57 green, 9 new in this commit)
- SignaturePageViewModelTests: constructors and dimension validation,
  Attach/Detach idempotence, StrokeCompleted and Clear propagate to
  the VM, CaptureAsync on empty buffer is a no-op, CaptureAsync on a
  non-empty buffer writes a v1 envelope with the expected
  structure (parsed back via JsonDocument, not text matching), and
  creates the destination directory if missing.
- All previously-green tests (48) remain green.

Out of scope
- POST /api/signature endpoint on Yavsc.Api (commit 3).
- SignalR handler that opens the page on a "devis received" push.
- Rasterization: this commit only proves capture and persistence;
  the visible ink is a Polyline reconstruction, not a PNG, by
  design (per the wire-format decision in commit 1).

Note on SignaturePadData
- The PointCount property was added after f9cfd560 landed. It is
  folded into this commit rather than amending f9cfd560 to keep
  the existing history readable; the change is mechanical and
  tested by the new SignaturePageViewModelTests.
2026-07-04 15:11:22 +01:00
f9cfd56089 a Signature Pad 2026-07-04 14:43:39 +01:00
587e98a5b2 presentation 2026-07-01 00:13:57 +01:00
db25b0772e bug fix IUserSettings 2026-06-30 23:55:43 +01:00
c3912a72ef fixes and renaming 2026-06-30 23:32:05 +01:00
728aa4198c Exploiting GitVersion 2026-06-28 14:11:26 +01:00
Lum
77b75916a7 postit: make Settings thread-safe and route PropertyChanged through UI dispatcher
The postit://callback re-launch crashed Avalonia inside
DataValidationErrors.SetErrors with 'The calling thread cannot
access this object because a different thread owns it'. Two
Settings instances raced on PropertyChanged: one was the DI
singleton registered by App.OnFrameworkInitializationCompleted,
the other was a freshly-constructed fallback in
LoginPageViewModel() and LoginPage.axaml.cs's DataContext-null
branch. Avalonia's binding sink caught the cross-thread
notification and crashed before the LoginPage could render.

Fix at three layers:

1. Settings: lock the mutation gate so concurrent Load() /
   ApplyJson() callers cannot tear reads; override
   OnPropertyChanged to marshal every notification onto the
   Avalonia UI thread via a new UiDispatcher helper (no more
   cross-thread SetErrors). Add BindToServiceProvider /
   RequireCurrent so production code paths cannot silently
   allocate a second instance.

2. LoginPageViewModel(): resolve the canonical Settings from
   the DI container (Settings.RequireCurrent) instead of
   new Settings(). The cross-thread crash is now caught loudly
   with a clear 'Settings.Current is not bound' error if
   something instantiates the VM outside a bound App.

3. HomePage.axaml.cs and LoginPage.axaml.cs: resolve the
   next view-model and BlogApiClient through App.Services
   instead of constructing them with 'new'. Same instance
   tree as the rest of the app; the postit://callback race
   disappears by construction.

MainPageViewModel and HomePageViewModel keep their existing
'?? new Settings()' fallback for test friendliness, but the
fallback is now harmless because Settings itself is
thread-safe.

Adds two regression tests in SettingsLoadTests covering
concurrent Load+mutate and concurrent idempotent Load.
2026-06-28 13:33:06 +01:00
b3ef12bce6 refactor(postit): rewrite MainPage layout in Grid
- ContentPage now stretches to fill the window
- root StackPanel replaced by a 3-row Grid (Auto, *, Auto)
  so the post list absorbs the middle band and the detail
  panel sits below
- detail panel inner StackPanel replaced by a Grid (Auto,
  Auto, Auto, *, Auto) so the AvaloniaEdit TextEditor fills
  all remaining width and height; MinHeight=320 keeps a
  sane floor on tiny windows
2026-06-28 01:47:12 +01:00
bbde168340 petit refacto 2026-06-28 01:43:39 +01:00
bdf7676875 postit: change MainPage base class from NavigationPage to ContentPage
HomePage.OnLoginClick does Navigation.PushAsync(new MainPage
{ ... }). NavigationPage.PushAsync only accepts a Page (or
Page subclass), not a MultiPage. MainPage was declared as

  public partial class MainPage : NavigationPage

in MainPage.axaml.cs and the root of MainPage.axaml was

  <NavigationPage ...>

which means 'new MainPage()' produced a MultiPage, not a
Page. PushAsync against a MultiPage argument does not route
through the standard Page push path; the visible result is
that the login succeeds, LoginSucceeded fires, but the UI
stays on LoginPage — the user is left looking at the
post-login state without any navigation.

The XAML content of MainPage (a StackPanel with the post
CRUD UI, a ListBox, a TextEditor) does not need the
multi-page container semantics — it's a single screen.
Switch both the code-behind base class and the XAML root
element to ContentPage so that MainPage is what
PushAsync expects.
2026-06-28 01:13:08 +01:00
38eb3922c4 postit: drop ConfigureAwait(false) after interactive login
LoginAsync uses ConfigureAwait(false) on the await of
LoginInteractiveCoreAsync. Since the surrounding code is
already executing on the UI thread (it was reached via a
RelayCommand that the UI dispatcher dispatched), the
ConfigureAwait drops the SynchronizationContext, and the
subsequent setters — IsBusy, AccessToken, StatusMessage,
LoginSuccess, and the LoginSucceeded?.Invoke() — run on a
thread-pool worker.

The downstream effects are all UI-bound: PropertyChanged
events fire, the BindingEngine republishes them as
AvaloniaObject.SetValue calls, and SetValue calls
Dispatcher.VerifyAccess. VerifyAccess throws because the
AvaloniaObject was created on the UI thread (owned by it)
and the SetValue is being attempted from a thread-pool
worker. Avalonia 11.12 throws SynchronousException through
DispatcherOperation.InvokeCore instead of dispatching back,
so the X11 message loop crashes the process with
System.InvalidOperationException: 'The calling thread cannot
access this object because a different thread owns it.'

Reproduced with the freshly installed postit_1.0.0-1_amd64.deb
package on a Debian 13 host — the .NET runtime loaded the
app, Avalonia started the X11 message loop, the operator
clicked 'Se connecter', the OIDC flow reached the post-login
phase, and the post-await setter chain crashed the process.

Drop ConfigureAwait(false) so the await captures the UI
thread SynchronizationContext and the setters resume on the
UI thread. The inner LoginInteractiveCoreAsync still uses
ConfigureAwait(false) for its own await, which is fine —
the inner method does not touch observables, only mutates
Platform.CreateBrowser and awaits the OIDC roundtrip, so it
can run anywhere.
2026-06-28 01:05:02 +01:00
7e8f552d67 tests: feed WebServerFixture a Smtp config so Authenticate fires 1.0.0
The EMaillingTests.SendEMailSynchrone smoke test asserts the
recording fake observed this exact call sequence on a successful
send:

  Connect, Authenticate, Send, Disconnect

MailSender.SendEmailAsync only calls Authenticate when
smtpSettings.UserName is non-null (src/Yavsc.Server/Services/
MailSender.cs line 89). WebServerFixture built the host without
a Smtp config — so UserName resolved to null, Authenticate was
skipped, and the recording captured only:

  Connect, Send, Disconnect

Pre-existing breakage, not introduced by recent work; the
fixture had been loading from .env indirectly (probably never,
or before a refactor that stopped doing so).

Feed the test host a fake SMTP config via the same
AddInMemoryCollection the fixture already uses for
ConnectionStrings:

  Smtp:Host     = smtp.test.local
  Smtp:Port     = 465
  Smtp:UserName = test-user
  Smtp:Password = test-pass

UserName non-null means MailSender now exercises the Authenticate
branch, which the recording captures. Tests in the Yavsc.Org.Tests
suite: 21/21 green (was 20/21 with SendEMailSynchrone failing).
2026-06-27 21:16:03 +01:00
a9643ba29d contributing+roadmap: document smoke tests per BC, tick off Jalon 0
- CONTRIBUTING.md 'Tests' section now describes the smoke
  pattern: per-BC, in-memory TestServer, EF InMemory, asserts
  2xx/3xx or 401/403 on a representative GET.
- ROADMAP.md 'Tests d'integration smoke par BC' flips from
  open to ticked off (Yavsc.Org coverage), with a note that
  Yavsc.Api and Yavsc.Blogs smoke coverage is left for a
  future session (separate WebApplicationFactory<Program>
  targets).

With this commit, Jalon 0 'Fondations techniques' is fully
ticked off. The release criterion
  'dotnet build + dotnet test + docker compose up verts sur
   une machine vierge (apres procedure d'install)'
is met end-to-end for Yavsc.Org; the docker-compose criterion
documents the cert/HTTPS requirement for web explicitly.
2026-06-27 21:04:26 +01:00
43ffd7b5aa tests: smoke tests for Account and Blog BCs
Two tests, two bounded contexts (BCs as enumerated in
doc/ddd-exploration-2026-06-14.md):

  - AccountSmokeTests : GET /signin
    YavscConstants.SigninPath = "~/signin"
    Routing + Razor + IdentityServer + EF + DI all wired.

  - BlogSmokeTests : GET /BlogSpot/Index
    BlogSpotController (note the capital S) under
    Controllers/Communicating/. No class-level [Route], so
    conventional /{controller}/{action} applies.

Both use TestWebApplicationFactory<Program> + the EF InMemory
provider wired by WebServerFixture.SetupHost, so they boot the
production HTTP pipeline without sockets, certs or a real DB.

Closes the 'Tests d'integration smoke par BC' item of Jalon 0
in ROADMAP.md (Yavsc.Api / Yavsc.Blogs coverage to come).
2026-06-27 21:03:16 +01:00
a265832282 tests: add SmokeTestBase helper for HTTP smoke assertions
Smoke tests for the Jalon 0 'Tests d'integration smoke par BC'
item need a small helper to:
- issue a GET on an in-memory test server (HttpClient built by
  TestWebApplicationFactory<Program>);
- assert that the response is 2xx (page served), 3xx (redirect
  to login) or 401/403 (anonymous rejected). Anything else —
  404 route missing, 5xx server crash, connection refused —
  fails the test.

This commit only introduces the base class. Subsequent commits
add the per-BC smoke tests (Account, Blog, etc.).
2026-06-27 20:52:08 +01:00
443ad96f17 roadmap: drop Centralisation des versions NuGet from Jalon 0
The work was actually 'centraliser les versions communes'
(shared packages), not 'centraliser toutes les versions'. That
work is done:

- Directory.Packages.props at the repo root declares shared
  package versions (ManagePackageVersionsCentrally=true,
  see e.g. coverlet.collector, HigginsSoft.IdentityServer8,
  IdentityModel.OidcClient, Microsoft.AspNetCore.*,
  xunit.v3, …);
- each product directory imports it via GetPathOfFileAbove
  and adds only product-specific versions.

No remaining work justifies the bullet. Removing it from
Jalon 0 leaves one open item: 'Tests d'integration smoke par BC'.
2026-06-27 18:47:42 +01:00
06449be755 CodeQL 2026-06-27 18:37:18 +01:00
6597626c5a Create codeql.yml 2026-06-27 18:30:07 +01:00
5e7e05c788 Create SECURITY.md 2026-06-27 18:28:42 +01:00
5310a8361e docker-compose: explicit source/target mapping for build secrets
The shorthand 'secrets: - yavsc_appsettings' relies on Compose
v2 to derive both source and target from the same name. In
some BuildKit integrations this is not enough — the secret id
seen inside the Dockerfile (yavsc_appsettings) and the source
defined at top-level (yavsc_appsettings, file: ...) end up not
being mapped correctly, leading to:

  cp: cannot stat '/run/secrets/yavsc_appsettings':
    No such file or directory

at the blogs-runtime / api-runtime / web-runtime stages.

Use the explicit long form:

  secrets:
    - source: yavsc_appsettings
      target: yavsc_appsettings

in all three runtime services. source is the top-level secret
name (file: ./src/Yavsc.Org/appsettings-org.json); target is
the id BuildKit exposes inside the container at
/run/secrets/yavsc_appsettings, matching --mount=type=secret,
id=yavsc_appsettings in the Dockerfile.
2026-06-27 18:25:26 +01:00
86a98187d5 ci: pass --target build-env to docker build in APK workflow
After the Dockerfile refactor into multi-stage (commit 6f975f87),
'docker build .' without --target selects the LAST stage of the
Dockerfile — which is blogs-runtime, an ASP.NET image with no
APK to extract. The subsequent docker cp command then fails
with:

  Error: No such container:path: …PostIt.Android/bin/Release/
  net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk

Add --target build-env to scope the build to the build-env stage
(the one that produces both the .apk and the publish artifacts,
and which still ends with CMD ["bash"]).
2026-06-27 18:21:42 +01:00