feat/app-invite #33

Closed
notazof wants to merge 220 commits from feat/app-invite into feat/postit-acl

220 commits

Author SHA1 Message Date
04a31709a2
refactor(postit): split IContactService from IUserDirectory
IContactService used to be the catch-all for "people you can reach
from PostIt": on mobile it read the device-local address book, on
desktop it queried the central /api/user-search endpoint and merged
both worlds into a single ContactDto (a flat Email field, an
ObservableCollection cache, a SearchAsync method). Two unrelated
flows under the same name, with a wire shape (Email) silently
flattening the mobile provider's multi-email list.

Split into two interfaces, each with a single responsibility:

- IContactService: device-local address book only. Mobile provider
  reads MAUI Essentials Contacts.Default and carries the full email
  list per contact. Desktop provider is an honest stub returning an
  empty list — the desktop has no local address book, and inviting
  external people from desktop is a separate flow (manual email
  entry + invitation endpoint) that doesn't belong here.

- IUserDirectory: central Yavsc user directory, the only consumer
  of /api/user-search. Both Desktop and Mobile providers delegate
  to UserSearchClient; the platform split exists so future
  platform-specific sources (offline cache, directory-scoped
  providers) can plug in without disturbing consumers.

ContactDto restores IReadOnlyList<string> Emails (the flat Email
from d0e0f4c1 was a regression that matched the wire shape of
/api/user-search at the cost of the mobile provider's per-contact
list). UserSummary is a separate platform-neutral record that
mirrors the server's UserSearchResultDto without leaking transport
concerns.

App.axaml.cs registers both interfaces as singletons.

Build + 51/51 PostIt.Tests green. No UI consumer yet — these
interfaces are still plomberie; the ViewModel that joins them for
the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
29a66a8c30 Merge branch 'feat/postit-acl' into feat/app-invite 2026-08-18 01:16:34 +01:00
da3534bd34 Merge branch 'main' into feat/postit-acl
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 15s
Dotnet build and test / build (pull_request) Successful in 5m44s
2026-08-18 01:03:03 +01:00
f4d4c786b4 Merge pull request 'release/1.0.6' (#28) from release/1.0.6 into main
All checks were successful
Dotnet build and test / log-the-inputs (push) Successful in 19s
Dotnet build and test / build (push) Successful in 7m33s
Reviewed-on: #28
2026-08-18 01:02:44 +01:00
dd8cb60fb4
Forgejo badges 2026-08-18 01:01:22 +01:00
d0e0f4c175
feat(postit): wire Desktop address book to /api/user-search
Replaces the empty ContactService.Desktop stub with a real
implementation backed by UserSearchClient. Closes the loop
between the server-side /api/user-search endpoint (b3056f1c),
the client wrapper (6e7e0414), and the platform abstraction.

IContactService gains:
- SearchAsync(string query, CancellationToken): on desktop,
  hits /api/user-search and appends results to an in-memory
  cache. On mobile, throws PlatformNotSupportedException —
  mobile providers use the device-local address book
  (GetDeviceContactsAsync) and don't talk to a network search.
- Contacts (ObservableCollection<ContactDto>): live view of
  the cache; UI binds directly to it. Mobile populates it
  inside GetDeviceContactsAsync (eager load); desktop populates
  it via SearchAsync (lazy, on-demand).

ContactDto shape changes:
- Emails (IReadOnlyList<string>) -> Email (string?). The
  /api/user-search endpoint returns one email per user. The
  use case ('invite / add to a circle') only needs one.
- Mobile provider flattens its per-contact email list down
  to the first non-empty entry (a small functional loss that
  matches the wire shape).

App.axaml.cs constructs a ContactService from the
UserSearchClient singleton and registers it as
IContactService so future ViewModels can take the interface
by constructor injection.

Build + 51/51 tests green. The mobile provider is still
gated by #if ANDROID || IOS and not exercised by the
Desktop test target — runtime behaviour on Android will
need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
6e7e04141b
feat(api-client): add UserSearchClient for /api/user-search
Adds the client-side half of the user-search endpoint landed
on the server in b3056f1c (commit 6 on this branch). The
client mirrors the server's filter contract:

- query: substring match on FullName or UserName
- email: exact match on Email
- take: 1..100, default 25

Empty (query + email) short-circuits to an empty list
client-side rather than letting the server return the first
`take` users alphabetically — the address-book UX is
"type to search", not "show me a directory".

The DTO (Yavsc.Api.Client.Dtos.UserSearchResultDto) is a flat
shape (Id, UserName, FullName, Avatar, Email) with no
navigation properties; field names match the JSON the server
emits so deserialisation is a no-op.

PostIt wiring:
- App.axaml.cs constructs a UserSearchClient singleton and
  registers it alongside CircleApiClient and BlogAclApiClient.
- The PostIt.csproj ProjectReference to Yavsc.Api.Client was
  in place before this commit on feat/postit-acl; the rebase
  of feat/app-invite on top of feat/postit-acl dropped it.
  This commit re-adds it.
2026-08-18 00:34:29 +01:00
69a660cafb
feat(app-invite): isolate ContactService to mobile targets
Splits the single ContactService class (which threw
PlatformNotSupportedException on non-Android/iOS targets) into a
platform-conditional structure:

- IContactService + ContactDto: shared abstraction in
  src/PostIt/PostIt/Services/IContactService.cs. ViewModels depend
  on this; concrete providers map their native shapes to ContactDto.

- ContactService.Mobile.cs: MAUI Essentials implementation, compiled
  only when ANDROID or IOS is defined. Wraps
  Contacts.Default.GetAllAsync() with permission handling and a
  NotImplementedInReferenceAssemblyException safety net.

- ContactService.Desktop.cs: stub returning an empty list, compiled
  when neither ANDROID nor IOS is defined. Replaces the
  'throw PlatformNotSupportedException' path so desktop targets
  (PostIt.Desktop, PostIt.Browser) build and run cleanly.

The Microsoft.Maui.Essentials portable facade is referenced from
PostIt.csproj, but it only becomes functional when the host
application project (PostIt.Android, future PostIt.iOS) also
references the platform-specific implementation.

No tests added: per AGENTS.md, a 'stub returns empty list' test on
PostIt.Tests (net10.0 desktop target) would be cosmetic and not
detect the real failure mode. Android-side tests require a working
PostIt.Android project, which doesn't exist yet.

Future providers (Google Contacts API, Exchange, CardDAV) plug in
as additional IContactService implementations selected by DI
configuration.
2026-08-18 00:31:26 +01:00
a8c219e0fa
WIP app invite: scaffold MAUI Essentials dependency in shared PostIt
Adds Microsoft.Maui.Essentials package and <UseMaui>true</UseMaui> to
src/PostIt/PostIt/PostIt.csproj so the shared project can compile code
that calls MAUI Essentials APIs (Microsoft.Maui.ApplicationModel.*).

Also adds a draft ContactService that wraps Contacts.Default.GetAllAsync()
behind a runtime platform check and permission request.

WIP caveats:
- The portable MAUI Essentials facade compiles on net10.0 but throws
  NotImplementedInReferenceAssemblyException at runtime when no
  platform-specific MAUI Essentials binary is loaded. A PostIt.Android
  project (or equivalent) must reference the Android MAUI Essentials
  implementation for Contacts.Default.GetAllAsync() to actually work.
- On desktop (Linux/macOS/Windows) the API is unsupported by design;
  ContactService currently throws PlatformNotSupportedException. A
  desktop stub returning Array.Empty<Contact>() is the likely next step.
- No tests yet. The scaffold is unverified at runtime; build passes.
2026-08-18 00:31:26 +01:00
ab8e77279b
ci(forgejo): put asset name in URL query string, not as curl arg
Le run #102 (re-publication du tag 1.0.6 après le fix jq + bump image v2)
a passé le PATCH /releases/10706 (jq a bien extrait l'id racine, plus
de 404), mais l'upload d'asset a planté avec un 400 "Missing 'name'
parameter".

Cause : sur l'appel curl de l'upload d'asset, l'argument `?name=...`
était passé en argument positionnel entre `--data-binary @file` et
l'URL. curl l'interprète comme un second fichier d'input (un fichier
nommé '?name=...'), pas comme un query param, et l'API Forgejo ne
voit jamais le name.

Fix : concaténer `?name=PostIt.Android.apk` à l'URL directement.
L'API Forgejo accepte le name en query string sur POST /releases/{id}/assets.
2026-08-18 00:31:25 +01:00
bea2e35bb4
chore(release): update 1.0.6 CHANGELOG section (image v2, jq fix)
La section [1.0.6] - stable du CHANGELOG mentionnait encore
debian12-dotnet10-android36-v1 et ne décrivait pas le fix du PATCH
release qui tombait en 404 à cause du sed greedy + JSON minifié.
Mets à jour avant de relancer la publication de la release
1.0.6 (workflow_dispatch), pour que le body publié reflète l'état
réel de l'infra (image v2 avec jq) et du workflow.
2026-08-18 00:31:25 +01:00
c2d55317ab
ci(forgejo): build JSON bodies with jq instead of hand-rolled sed
L'image runner pazof/yavsc-build-env installe jq (>= 1.7) à partir
de debian12-dotnet10-android36-v2 (Dockerfile du repo
dotnet-android-build-image, commit e06f096 "adds jq"). On en
profite pour supprimer json_escape et json_field à base de sed,
qui étaient fragiles :

  * sed est greedy par défaut : sur du JSON minifié d'une seule
    ligne (ce que renvoie l'API Forgejo de cette instance pour
    /releases/tags/<tag>), la regex s/.*"id".../\1/p attrape la
    DERNIÈRE occurrence de "id":<digits> sur la ligne, qui est
    l'id de l'auteur de la release (1, premier user du repo),
    pas l'id de la release (10706).
  * Le head -3 ajouté en PR #30 ne tient pas sur du JSON minifié :
    il n'isole rien et le sed greedy continue à capturer
    l'id de l'auteur.
  * PATCH /releases/1 tombait alors en 404 "The target couldn't
    be found" (cf. run échoué du 2026-08-17 04:05 sur le tag
    1.0.6).

jq résout les deux problèmes en une fois :
  * jq -r '.id' retourne le champ id racine, pas l'id imbriqué
    dans author.
  * jq -n --arg body "$RELEASE_BODY" '{body: $body, prerelease:
    $prerelease}' construit un body JSON proprement échappé
    (backslashes, guillemets, newlines, caractères de contrôle
    Unicode) sans avoir à le reproduire à la main.

Effet de bord : les bodies PATCH et POST sont écrits dans
/tmp/patch.json et /tmp/post.json puis passés à curl via
--data-binary @<file> au lieu d'une variable shell. Plus de
problème de quoting en chaîne shell, plus de collision avec
les espaces ou les caractères spéciaux du body.

Pré-requis côté runner : image pazof/yavsc-build-env:debian12-
dotnet10-android36-v2 (avec jq) + maj du label correspondant
dans la config du runner Forgejo.
2026-08-18 00:31:24 +01:00
24fede0bd0
ci(forgejo): limit json_field extraction to top-level keys
L'API Forgejo renvoie pour /releases/tags/<tag> un objet JSON
pretty-printed où l'id racine (release.id, ex. 10706) est sur la
première ligne, mais l'objet author contient aussi un id (souvent 1
pour le premier user du repo). L'ancienne regex sed matchait la
première occurrence globale de "id" dans le fichier, donc elle
retombait sur author.id=1 et le PATCH /releases/1 tombait en 404
'The target couldn't be found'.

Fix : on pipe le fichier dans 'head -3' pour ne matcher que les
premières lignes (couvre largement le préambule de l'objet release).
Si Forgejo renvoie du JSON minifié (une seule ligne), head -3
renvoie toute la ligne et la regex matche le premier id (la racine,
parce que les champs auteur sont après les champs racine).
2026-08-18 00:31:24 +01:00
2df364aa1e
ci(forgejo): build JSON bodies in pure bash, no python3
L'image runner pazof/yavsc-build-env n'a pas python3 (ni jq, ni
node). Le step de publication Forgejo utilisait python3 pour générer
les bodies JSON (POST /releases, PATCH /releases/{id}) et pour
extraire le 'id' de la réponse.

Fix : deux fonctions bash :
- json_escape : escaping JSON des chaînes (\\, \", \n, \r, \t)
- json_field : extraction d'un champ scalaire d'un fichier JSON via sed

Suffisant pour les bodies qu'on envoie (tag_name, name, body,
prerelease) et les champs qu'on lit (id).
2026-08-18 00:31:23 +01:00
copilot-swe-agent[bot]
8960ce7d93
fix(ci): fix validate-release CHANGELOG channel check to inspect heading line
Co-authored-by: pazof <3072814+pazof@users.noreply.github.com>
2026-08-18 00:31:23 +01:00
copilot-swe-agent[bot]
5c20c0bc04
Initial plan 2026-08-18 00:31:23 +01:00
64d25bb2f1
ci(forgejo): build .NET projects directly, skip docker
L'image runner pazof/yavsc-build-env a le SDK .NET 10 et le workload
Android, mais PAS le binaire 'docker' ni de daemon Docker. Le
'Build de l'image Docker' du workflow plantait avec 'docker: command
not found'.

Fix : on exécute directement les commandes dotnet du Dockerfile
(restore + build Yavsc.Org/Api/Blogs + build PostIt.Android -r
android-arm64), puis on copie l'APK depuis le chemin de sortie
standard bin/Release/net10.0-android/android-arm64/.

Note : le Dockerfile reste la voie canonique pour les builds en
local et via GitHub Actions (qui a docker). Ce fix concerne
uniquement le workflow Forgejo Actions où le runner n'a pas Docker.
2026-08-18 00:31:22 +01:00
e07c536e1f
ci(forgejo): check CHANGELOG channel suffix on the section title
The previous awk extracted the section body but excluded the title
line (## [TAG] - channel), so the '* - $CHANNEL*' pattern never
matched. Fix: include the title line in the extracted body, verify
the channel suffix on the title, then strip the title before passing
the body to the release API.
2026-08-18 00:31:22 +01:00
fd99260bc7
ci(forgejo): replace all Node-based actions with bash + curl
The runner's docker label points at pazof/yavsc-build-env, a Debian
image without Node.js. Any action like actions/checkout@v7,
actions/upload-artifact@v7, rasterstate/forgejo-release-action, etc.
fails at container start with 'executable file not found in /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/paul/.dotnet/tools:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/home/paul/.nvm/versions/node/v22.23.0/bin:/home/paul/.local/bin:/home/paul/.npm-global/bin:/home/paul/bin:/home/paul/.nix-profile/bin'.

This workflow is rewritten in pure bash:
- replace actions/checkout with explicit git clone + checkout (full
  history + tags so GitVersion.MsBuild is happy);
- merge the two jobs into one (no inter-job artifacts needed since
  everything shares the runner's filesystem);
- replace rasterstate/forgejo-release-action with direct calls to the
  Forgejo REST API (/api/v1/repos/.../releases, .../assets), with
  python3 used to build and parse JSON bodies (jq not guaranteed in
  the runner image).

Auth: ${{ secrets.GITHUB_TOKEN }} (runner-provided). The
rasterstate action or any other Node-based action can be reinstated
later if the runner image is swapped for one with Node installed.
2026-08-18 00:31:21 +01:00
c4695dc254
ci(forgejo): use runner-provided GITHUB_TOKEN for release workflow
Repo-level secrets creation is broken on this Forgejo instance
(InsertEncryptedSecret fails with UTF-8 byte-sequence error, likely
a text-vs-bytea column type on the secret table). The fix is in
upstream Forgejo v16; until then, ${{ secrets.GITHUB_TOKEN }} (auto-
provided by the runner, scoped to contents: write for the current
repo) keeps the release workflow operational without any UI setup.

When the instance is upgraded and the secret table is migrated,
revert this commit to switch back to ${{ secrets.RELEASE_TOKEN }}
for least-privilege.
2026-08-18 00:31:21 +01:00
4a15edb9e5
ci(forgejo): publish release with PostIt APK on tag push
Adds .forgejo/workflows/release.yml: triggered by tag push or
workflow_dispatch, it validates the tag/CHANGELOG parity (stable /
preview / unstable), builds the PostIt Android APK via the existing
Dockerfile (--target build-env), and publishes a Forgejo release with
the APK as an asset via rasterstate/forgejo-release-action@v1.

Mirrors the validate-release logic of .github/workflows/docker-publish-android.yml
so the two channels (Forgejo source-of-truth + GitHub mirror) stay
consistent. Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo
PAT scoped to write:repository configured in the repository's Actions
secrets.
2026-08-18 00:31:19 +01:00
b3056f1c2e
feat(user-search): add UserSearchApiController in Yavsc.Blogs
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 26s
Dotnet build and test / build (pull_request) Successful in 14m59s
Lives in Yavsc.Blogs (not Yavsc.Api) because Yavsc.Api is not
yet enabled in production; future migration to Yavsc.Api is a
single namespace + route prefix change.

Endpoint: GET /api/user-search?q=<name>&e=<email>&take=<n>
- Authorisation: [Authorize] (any authenticated caller).
- q: case-insensitive substring match on FullName OR UserName.
- e: case-insensitive exact match on Email.
- take: 1..100, default 25.

Returns a flat UserSearchResultDto (Id, UserName, FullName,
Avatar, Email) — no navigation properties, so the payload
stays small even if the user table grows.

The Email field is included because the address-book use case
(composing circle membership, sending invites) needs it.
On Yavsc's single-tenant deployments the user table is a
closed community; multi-tenant deployments should gate this
controller behind a tenant-scoped policy before exposing it.
The trade-off is documented in the controller's class-level
XML doc.
2026-08-18 00:20:10 +01:00
1b289c1387
refactor(model): rename Yavsc.Blogspot.BlogPost to BlogPostDto
When commit 0e95e283 moved BlogPost from PostIt.Models to
Yavsc.Blogspot, it created an unfortunate collision with the
server-side EF entity Yavsc.Models.Blog.BlogPost. The two
classes have nothing in common beyond the name; the DTO is
the wire shape PostIt exchanges with the Blogs API, the EF
entity is the persistence model. Server code that imports both
namespaces (BlogSpotService.cs, etc.) ended up with 'BlogPost
is an ambiguous reference between X and Y' errors.

Renaming the client DTO to BlogPostDto (matching the
naming convention of the other DTOs in Yavsc.Api.Client.Dtos
— CircleDto, CircleAuthorizationDto, UserSearchResultDto)
disambiguates without renaming the EF entity on the server.

The namespace stays Yavsc.Blogspot; only the class name
changes. All call sites (client code, tests, XAML DataTemplates,
XML doc comments) are updated mechanically.
2026-08-18 00:20:01 +01:00
0e7576857d
feat(postit): UI for managing Circles + per-post ACL
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 13s
Dotnet build and test / build (pull_request) Failing after 3m51s
Landing the user-facing surface for the BlogAcl work. The user
can now:

1. Open the 'Mes cercles' page (a new 'Mes cercles' button on
   the main page) and create / edit / delete their own
   circles. The page lists circles in an ObservableCollection
   bound to a ListBox; per-row buttons drive StartEdit and
   Delete; the bottom editor pushes new / edited circles via
   the Save command.

2. With a post selected, click the new 'ACL' button to open a
   modal 'PostAclDialog' for that post. The modal shows the
   current ACL entries (filtered server-side by Allowed.OwnerId
   == caller) and a dropdown of the caller's circles to add.
   Each entry has a 'Revoke' button.

Both pages follow the same pattern:
- ViewModel uses [ObservableProperty] for state and
  [RelayCommand] for verbs; IsBusy drives a ProgressBar
  overlay; StatusMessage surfaces server feedback.
- View follows the XAML-Background/Foreground lesson (no
  hard-coded colours), so dark mode works without
  contrast surprises.
- Code-behind is minimal — just AvaloniaXamlLoader.Load —
  because navigation is driven by RelayCommand + event
  (ManageAclRequested, OpenCirclesRequested) that the
  MainPage code-behind handles via its DataContextChanged
  handler.

The 'complete' scope (c) of this commit was confirmed by
Paul. Three follow-up tracks are deliberately out of scope
and tracked in MEMORY.md (2026-08-18):
- i18n: no .resx / IStringLocalizer today; all visible text
  is hard-coded French.
- Avalonia.Headless UI tests: only ViewModel-level coverage
  is feasible today; full navigation tests are a separate
  effort.
- XAML accessibility audit of pre-existing pages (Settings,
  MainPage) that predate the Background/Foreground lesson.

Build + 51/51 tests green.
2026-08-18 00:06:57 +01:00
a5887a2387
feat(postit): wire Circle + BlogAcl clients in the DI container
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 21s
Dotnet build and test / build (pull_request) Failing after 7m24s
App.axaml.cs is the composition root for PostIt. It now also
builds and registers:
- CircleApiClient (singleton) — backed by the same YavscApiClient
  and the same blogs base URL as BlogApiClient
- BlogAclApiClient (singleton) — same shape
- IYavscApiClient -> YavscApiClient mapping (singleton). The
  concrete class is still resolvable as YavscApiClient; the new
  registration makes the same instance available as
  IYavscApiClient so future consumers (and unit tests) can take
  the interface without coupling to the concrete type.

The 3 high-level clients are singletons: they hold no mutable
state of their own, just a reference to YavscApiClient and a
base URL. Reusing the same instance across requests is what the
HttpClient inside YavscApiClient was already designed for.
2026-08-17 23:51:51 +01:00
f835ad42a1
feat(api-client): add Yavsc.Api.Client with Blog + Circle + BlogAcl clients
Creates the high-level HTTP client library the PostIt UI will
consume to manage blog posts, circles, and per-post ACLs.

Clients in this commit:
- BlogApiClient (moved from PostIt/Services; same public surface,
  now depends on IYavscApiClient instead of the concrete class).
- CircleApiClient (new): GET/POST/PUT/DELETE /api/circle. Takes
  the blogs base URL explicitly in its constructor so it doesn't
  need to know about PostIt's Settings type.
- BlogAclApiClient (new): GET/POST/PUT/DELETE /api/blogacl.
  Same conventions as CircleApiClient.

DTOs (Yavsc.Api.Client.Dtos):
- CircleDto: id, name, ownerId, public. Stops short of the
  navigation properties on the server-side Circle (Owner,
  Members), which depend on ApplicationUser and other server
  types we don't want to drag into the client.
- CircleAuthorizationDto: circleId, blogPostId, comment. Same
  reason: the server entity has Target and Allowed navigation
  properties the client never needs.

The clients now require the caller to pass the blogs base URL
explicitly in the constructor (previously the BlogApiClient
sniffed it off YavscApiClient.Settings.BlogsApiUrl, but that
field is PostIt-specific). The one production call site
(App.axaml.cs) and four test call sites are updated to pass
the URL.

Build + 51/51 tests green. The IYavscApiClient abstraction was
landed in the previous commit so this one could be a pure
addition + relocation.
2026-08-17 23:50:35 +01:00
ab40af8ef1
refactor(api-client): introduce IYavscApiClient abstraction in Yavsc.Api.Client
Yavsc.Api.Client is the new home for high-level HTTP clients
(BlogApiClient, CircleApiClient, BlogAclApiClient, etc.). It
depends on the host application's transport layer, but the host
(PostIt) is a UI app with OIDC, settings, and an ApplicationData
directory — none of which the abstract client library should
know about.

The IYavscApiClient interface captures just the transport
surface those clients need:
- HttpClient (so the client can configure BaseAddress)
- CallAsync<T> and CallAsync (the JSON over HTTP verb)

It deliberately leaves out LoginAsync / TrySilentLoginAsync /
CurrentAccessToken / HasValidSession / Settings — those are
authentication and configuration concerns, not transport. They
stay on the concrete YavscApiClient in PostIt.Services.

The concrete YavscApiClient now implements IYavscApiClient; the
existing public surface is unchanged (no breaking changes for
existing call sites in PostIt or the tests).

This commit only lays the foundation. The actual high-level
clients (Blog/Circle/BlogAcl) land in a follow-up commit that
re-uses this interface, so this one stays a small, reviewable
refactor.
2026-08-17 23:50:24 +01:00
0e95e28327
refactor(model): move BlogPost DTO from PostIt.Models to Yavsc.Blogspot
BlogPost is shared between the server (Yavsc.Server/Models/Blog/
BlogPost.cs is the EF entity) and any client that talks to the
blogs API. Keeping the client-side DTO in PostIt.Models made
sense when there was only one consumer; now that the
Yavsc.Api.Client project is about to host BlogApiClient alongside
CircleApiClient and BlogAclApiClient, the DTO has to live in a
layer both the client project and PostIt can reference without
inverting the dependency.

Yavsc.Abstract is the existing home for cross-tier interfaces
and DTOs (IBlogPost, IBlogPostPayLoad, IApplicationUser).
Yavsc.Blogspot is the sub-namespace already used by the
matching interface, so the new concrete class follows.

Why not move Circle and CircleAuthorizationToBlogPost at the
same time? Both depend on the concrete ApplicationUser class
(via the Owner and Target/Allowed navigation properties) which
lives in Yavsc.Server. Moving them would mean either dragging
ApplicationUser into the abstract layer (huge blast radius —
auth, billing, chat, etc.) or weakening the navigation
properties (breaks EF Core shaping). They're staying where
they are; the new Yavsc.Api.Client will get DTO counterparts
instead.

Updated call sites:
- 4 .cs files: replace 'using PostIt.Models;' with
  'using Yavsc.Blogspot;' where the file was actually using
  BlogPost. Files that only used SignaturePadData keep their
  'using PostIt.Models;' — that type stays put.
- 1 .axaml file: xmlns:models="using:PostIt.Models" ->
  xmlns:models="using:Yavsc.Blogspot" (one DataTemplate for
  the post list in MainPage).

Build + tests green (51/51).
2026-08-17 23:45:45 +01:00
e376aed887
fix(blogacl): restrict Circle + BlogAcl reads and writes to caller's own data
Closes the data-leak holes that survived the move of these controllers
from Yavsc.Api to Yavsc.Blogs. Circles are personal — a circle and its
membership should never be visible, modifiable, or deletable by anyone
other than its owner.

BlogAclApiController:
- GetBlogACL() was returning the full table; now filters by
  Allowed.OwnerId == caller's uid, with an Include(a => a.Allowed)
  so EF Core can push the filter into SQL instead of materialising
  the whole table.
- Other endpoints (GetById, Put, Post, Delete) already enforced
  ownership; left as is.

CircleApiController:
- GetCircle() (no id) now filters by OwnerId.
- GetCircle(id) now requires c.Id == id && c.OwnerId == uid;
  returns 404 (not 403) on miss to avoid leaking the existence of
  someone else's circle.
- PutCircle verifies the existing record is owned by the caller,
  then forces circle.OwnerId = uid on the body (the client's value
  is ignored). Returns ChallengeResult when the caller doesn't own
  the record.
- PostCircle forces circle.OwnerId = uid (was trusting the body).
- DeleteCircle now filters by OwnerId; 404 on miss.

All checks use the same source of truth (User.FindFirstValue(
ClaimTypes.NameIdentifier)) that the existing BlogAclApiController
authz code already relies on.
2026-08-17 23:36:20 +01:00
40e5630cfc
refactor(blogacl): move BlogAcl + Circle controllers from Yavsc.Api to Yavsc.Blogs
These two controllers belong to the Blogs subsystem (their routes
/api/blogacl and /api/circle are blog-domain concerns, not the
generic Api surface). Moving them next to BlogApiController keeps
related code together and prepares the PostIt client to consume
them through the same BlogsApiUrl base address as the existing
BlogApiClient.

Mechanical changes only:
- Namespace Yavsc.Controllers -> Yavsc.Blogs.Controllers
- Drop unused 'using Yavsc.Helpers;' (no symbol in the new
  compilation unit depends on it; the build confirms it was
  dead since the controllers were first written)
- Fix typo in CircleApiController route: 'api/cirle' -> 'api/circle'
  (any client trying to call the documented route was hitting 404)

No functional changes to authorization or query shape. The known
security gaps in these controllers (GetBlogACL and GetCircle
return unfiltered collections, DeleteCircle has no ownership
check) are deliberately left untouched in this commit and will
be addressed in a follow-up.
2026-08-17 23:34:48 +01:00
a4792a7a83
ci(forgejo): put asset name in URL query string, not as curl arg
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 46s
Dotnet build and test / build (pull_request) Successful in 12m18s
Forgejo Release / release (push) Successful in 6m34s
Le run #102 (re-publication du tag 1.0.6 après le fix jq + bump image v2)
a passé le PATCH /releases/10706 (jq a bien extrait l'id racine, plus
de 404), mais l'upload d'asset a planté avec un 400 "Missing 'name'
parameter".

Cause : sur l'appel curl de l'upload d'asset, l'argument `?name=...`
était passé en argument positionnel entre `--data-binary @file` et
l'URL. curl l'interprète comme un second fichier d'input (un fichier
nommé '?name=...'), pas comme un query param, et l'API Forgejo ne
voit jamais le name.

Fix : concaténer `?name=PostIt.Android.apk` à l'URL directement.
L'API Forgejo accepte le name en query string sur POST /releases/{id}/assets.
2026-08-17 05:44:12 +01:00
77fda10347
chore(release): update 1.0.6 CHANGELOG section (image v2, jq fix)
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 8s
Dotnet build and test / build (pull_request) Failing after 7m7s
Forgejo Release / release (push) Failing after 10m7s
La section [1.0.6] - stable du CHANGELOG mentionnait encore
debian12-dotnet10-android36-v1 et ne décrivait pas le fix du PATCH
release qui tombait en 404 à cause du sed greedy + JSON minifié.
Mets à jour avant de relancer la publication de la release
1.0.6 (workflow_dispatch), pour que le body publié reflète l'état
réel de l'infra (image v2 avec jq) et du workflow.
2026-08-17 05:25:20 +01:00
b390eb7be9 Merge pull request 'fix/forgejo-release-json-field-id' (#31) from fix/forgejo-release-json-field-id into release/1.0.6
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 14s
Dotnet build and test / build (pull_request) Successful in 9m40s
Reviewed-on: #31
2026-08-17 05:07:39 +01:00
c3c54ba5d5
ci(forgejo): build JSON bodies with jq instead of hand-rolled sed
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 8s
Dotnet build and test / build (pull_request) Successful in 13m27s
L'image runner pazof/yavsc-build-env installe jq (>= 1.7) à partir
de debian12-dotnet10-android36-v2 (Dockerfile du repo
dotnet-android-build-image, commit e06f096 "adds jq"). On en
profite pour supprimer json_escape et json_field à base de sed,
qui étaient fragiles :

  * sed est greedy par défaut : sur du JSON minifié d'une seule
    ligne (ce que renvoie l'API Forgejo de cette instance pour
    /releases/tags/<tag>), la regex s/.*"id".../\1/p attrape la
    DERNIÈRE occurrence de "id":<digits> sur la ligne, qui est
    l'id de l'auteur de la release (1, premier user du repo),
    pas l'id de la release (10706).
  * Le head -3 ajouté en PR #30 ne tient pas sur du JSON minifié :
    il n'isole rien et le sed greedy continue à capturer
    l'id de l'auteur.
  * PATCH /releases/1 tombait alors en 404 "The target couldn't
    be found" (cf. run échoué du 2026-08-17 04:05 sur le tag
    1.0.6).

jq résout les deux problèmes en une fois :
  * jq -r '.id' retourne le champ id racine, pas l'id imbriqué
    dans author.
  * jq -n --arg body "$RELEASE_BODY" '{body: $body, prerelease:
    $prerelease}' construit un body JSON proprement échappé
    (backslashes, guillemets, newlines, caractères de contrôle
    Unicode) sans avoir à le reproduire à la main.

Effet de bord : les bodies PATCH et POST sont écrits dans
/tmp/patch.json et /tmp/post.json puis passés à curl via
--data-binary @<file> au lieu d'une variable shell. Plus de
problème de quoting en chaîne shell, plus de collision avec
les espaces ou les caractères spéciaux du body.

Pré-requis côté runner : image pazof/yavsc-build-env:debian12-
dotnet10-android36-v2 (avec jq) + maj du label correspondant
dans la config du runner Forgejo.
2026-08-17 04:35:00 +01:00
2f443e5450 Merge pull request 'ci(forgejo): limit json_field extraction to top-level keys' (#30) from fix/forgejo-release-json-field-id into release/1.0.6
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 20s
Dotnet build and test / build (pull_request) Successful in 5m7s
Forgejo Release / release (push) Failing after 10m25s
Reviewed-on: #30
2026-08-17 03:26:53 +01:00
5186ffb7c8
ci(forgejo): limit json_field extraction to top-level keys
L'API Forgejo renvoie pour /releases/tags/<tag> un objet JSON
pretty-printed où l'id racine (release.id, ex. 10706) est sur la
première ligne, mais l'objet author contient aussi un id (souvent 1
pour le premier user du repo). L'ancienne regex sed matchait la
première occurrence globale de "id" dans le fichier, donc elle
retombait sur author.id=1 et le PATCH /releases/1 tombait en 404
'The target couldn't be found'.

Fix : on pipe le fichier dans 'head -3' pour ne matcher que les
premières lignes (couvre largement le préambule de l'objet release).
Si Forgejo renvoie du JSON minifié (une seule ligne), head -3
renvoie toute la ligne et la regex matche le premier id (la racine,
parce que les champs auteur sont après les champs racine).
2026-08-17 03:26:11 +01:00
3222c56ddb Merge pull request 'ci(forgejo): build JSON bodies in pure bash, no python3' (#29) from fix/forgejo-release-bash-json into release/1.0.6
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 19s
Dotnet build and test / build (pull_request) Successful in 5m20s
Forgejo Release / release (push) Failing after 7m52s
Reviewed-on: #29
2026-08-17 02:18:09 +01:00
bbe483cb24
ci(forgejo): build JSON bodies in pure bash, no python3
L'image runner pazof/yavsc-build-env n'a pas python3 (ni jq, ni
node). Le step de publication Forgejo utilisait python3 pour générer
les bodies JSON (POST /releases, PATCH /releases/{id}) et pour
extraire le 'id' de la réponse.

Fix : deux fonctions bash :
- json_escape : escaping JSON des chaînes (\\, \", \n, \r, \t)
- json_field : extraction d'un champ scalaire d'un fichier JSON via sed

Suffisant pour les bodies qu'on envoie (tag_name, name, body,
prerelease) et les champs qu'on lit (id).
2026-08-17 02:16:46 +01:00
63c7dbbd9b
Merge remote-tracking branch 'github/main' into fix/forgejo-release-build-no-docker
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 35s
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-17 02:05:51 +01:00
739cd716ec
Merge pull request #70 from pazof/copilot/fix-validate-release-job
fix(ci): validate-release channel check always failed for stable/preview tags
2026-08-17 02:05:01 +01:00
copilot-swe-agent[bot]
843d6b227f
fix(ci): fix validate-release CHANGELOG channel check to inspect heading line
Co-authored-by: pazof <3072814+pazof@users.noreply.github.com>
2026-08-17 01:02:14 +00:00
copilot-swe-agent[bot]
704f7565fe
Initial plan 2026-08-17 01:00:12 +00:00
f5b1ccee5e Merge pull request 'ci(forgejo): build .NET projects directly, skip docker' (#27) from fix/forgejo-release-build-no-docker into release/1.0.6
Some checks failed
Forgejo Release / release (push) Failing after 10m11s
Reviewed-on: #27
2026-08-17 01:56:42 +01:00
44edf71b12
ci(forgejo): build .NET projects directly, skip docker
L'image runner pazof/yavsc-build-env a le SDK .NET 10 et le workload
Android, mais PAS le binaire 'docker' ni de daemon Docker. Le
'Build de l'image Docker' du workflow plantait avec 'docker: command
not found'.

Fix : on exécute directement les commandes dotnet du Dockerfile
(restore + build Yavsc.Org/Api/Blogs + build PostIt.Android -r
android-arm64), puis on copie l'APK depuis le chemin de sortie
standard bin/Release/net10.0-android/android-arm64/.

Note : le Dockerfile reste la voie canonique pour les builds en
local et via GitHub Actions (qui a docker). Ce fix concerne
uniquement le workflow Forgejo Actions où le runner n'a pas Docker.
2026-08-17 01:56:01 +01:00
69677727cb Merge pull request 'ci(forgejo): check CHANGELOG channel suffix on the section title' (#26) from fix/forgejo-release-changelog-title-check into release/1.0.6
Some checks failed
Forgejo Release / release (push) Failing after 13s
Reviewed-on: #26
2026-08-17 01:50:17 +01:00
5e600c11e1
ci(forgejo): check CHANGELOG channel suffix on the section title
The previous awk extracted the section body but excluded the title
line (## [TAG] - channel), so the '* - $CHANNEL*' pattern never
matched. Fix: include the title line in the extracted body, verify
the channel suffix on the title, then strip the title before passing
the body to the release API.
2026-08-17 01:49:45 +01:00
f2776b34e3 Merge pull request 'ci(forgejo): rewrite release workflow in pure bash + curl' (#25) from fix/forgejo-release-native-bash into release/1.0.6
Some checks failed
Forgejo Release / release (push) Failing after 13s
Reviewed-on: #25
2026-08-17 01:45:54 +01:00
5b957c6cbb
ci(forgejo): replace all Node-based actions with bash + curl
The runner's docker label points at pazof/yavsc-build-env, a Debian
image without Node.js. Any action like actions/checkout@v7,
actions/upload-artifact@v7, rasterstate/forgejo-release-action, etc.
fails at container start with 'executable file not found in /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/paul/.dotnet/tools:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/home/paul/.nvm/versions/node/v22.23.0/bin:/home/paul/.local/bin:/home/paul/.npm-global/bin:/home/paul/bin:/home/paul/.nix-profile/bin'.

This workflow is rewritten in pure bash:
- replace actions/checkout with explicit git clone + checkout (full
  history + tags so GitVersion.MsBuild is happy);
- merge the two jobs into one (no inter-job artifacts needed since
  everything shares the runner's filesystem);
- replace rasterstate/forgejo-release-action with direct calls to the
  Forgejo REST API (/api/v1/repos/.../releases, .../assets), with
  python3 used to build and parse JSON bodies (jq not guaranteed in
  the runner image).

Auth: ${{ secrets.GITHUB_TOKEN }} (runner-provided). The
rasterstate action or any other Node-based action can be reinstated
later if the runner image is swapped for one with Node installed.
2026-08-17 01:45:22 +01:00
19da7909ce Merge pull request 'ci(github): backport fetch-depth fix on apk-deploy checkout to release/1.0.6' (#24) from fix/github-apk-checkout-fetch-depth-backport into release/1.0.6
Some checks failed
Forgejo Release / validate-release (push) Failing after 10s
Forgejo Release / release (push) Has been skipped
Reviewed-on: #24
2026-08-17 01:37:50 +01:00
b69c382beb
Checkout
1. complet de l’historique Git et des tags dans le job qui build l’APK via Docker:
2. with tags
2026-08-17 01:36:55 +01:00
c5c4d6b59a Merge pull request 'ci(forgejo): use runner-provided GITHUB_TOKEN for release workflow' (#23) from fix/forgejo-release-use-runner-token into release/1.0.6
Reviewed-on: #23
2026-08-17 01:25:13 +01:00
80cb8c46fc
ci(forgejo): use runner-provided GITHUB_TOKEN for release workflow
Repo-level secrets creation is broken on this Forgejo instance
(InsertEncryptedSecret fails with UTF-8 byte-sequence error, likely
a text-vs-bytea column type on the secret table). The fix is in
upstream Forgejo v16; until then, ${{ secrets.GITHUB_TOKEN }} (auto-
provided by the runner, scoped to contents: write for the current
repo) keeps the release workflow operational without any UI setup.

When the instance is upgraded and the secret table is migrated,
revert this commit to switch back to ${{ secrets.RELEASE_TOKEN }}
for least-privilege.
2026-08-17 01:23:44 +01:00
70a69779fa Merge pull request 'ci(forgejo): publish release with PostIt APK on tag push' (#22) from feat/forgejo-release-page into release/1.0.6
Some checks failed
Forgejo Release / validate-release (push) Failing after 23s
Forgejo Release / release (push) Has been skipped
Reviewed-on: #22
2026-08-17 00:53:59 +01:00
3dd4700404
ci(forgejo): publish release with PostIt APK on tag push
Adds .forgejo/workflows/release.yml: triggered by tag push or
workflow_dispatch, it validates the tag/CHANGELOG parity (stable /
preview / unstable), builds the PostIt Android APK via the existing
Dockerfile (--target build-env), and publishes a Forgejo release with
the APK as an asset via rasterstate/forgejo-release-action@v1.

Mirrors the validate-release logic of .github/workflows/docker-publish-android.yml
so the two channels (Forgejo source-of-truth + GitHub mirror) stay
consistent. Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo
PAT scoped to write:repository configured in the repository's Actions
secrets.
2026-08-17 00:46:51 +01:00
af8762d085 Merge pull request 'fix/workflow-checkout-fetch-depth' (#21) from fix/workflow-checkout-fetch-depth into main
All checks were successful
Dotnet build and test / log-the-inputs (push) Successful in 16s
Dotnet build and test / build (push) Successful in 6m46s
Reviewed-on: #21
2026-08-17 00:15:45 +01:00
ed689cde96
Merge pull request #69 from pazof/fix/workflow-checkout-fetch-depth
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Successful in 6m45s
Fix/workflow checkout fetch depth
2026-08-17 00:09:25 +01:00
0fc3b81a0f
Checkout
1. complet de l’historique Git et des tags dans le job qui build l’APK via Docker:
2. with tags
2026-08-17 00:06:32 +01:00
56f115fa3e Merge pull request 'ci(release): re-check release/1.0.6 with billing init fix' (#20) from fix/post-release-recheck into release/1.0.6
Reviewed-on: #20
2026-08-16 16:52:11 +01:00
b3d1911302
ci: re-check CI on release/1.0.6 with billing init fix 2026-08-16 16:49:44 +01:00
f74577f80a Merge pull request 'fix(billing): tolerate ReflectionTypeLoadException during init' (#19) from fix/billing-init-reflection into release/1.0.6
Reviewed-on: #19
2026-08-16 16:43:09 +01:00
7f84d4d97a
fix(billing): tolerate ReflectionTypeLoadException during init
ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies()
and calls Assembly.GetTypes() on each. If any of the loaded assemblies
has a type that fails to resolve (a flaky dependency, an AddOn with a
broken reference, a test dependency that's been rewritten after compile),
GetTypes() throws ReflectionTypeLoadException (or, less commonly,
FileNotFoundException / TypeLoadException for the assembly itself).

In CI on the forgejo-runner (and especially in test discovery under
xunit v3), one such assembly is loaded somewhere between test runs and
silently throws. The exception is not handled, so:

  1. Collections are Cleared at the top of ConfigureBillingService().
  2. The reflection loop throws before reaching the
     RegisterBilling<HairCutQuery/HairMultiCutQuery/RdvQuery> calls.
  3. BillingService.Billing ends up empty (Count = 0).
  4. The second ConfigureBillingService() call sees the same assembly
     loaded (xunit v3 keeps the AppDomain warm for the whole suite),
     throws identically, and the test
     Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing
     fails with 'Assert.Equal() Failure: Expected 3, Actual 0'.

Fix: catch ReflectionTypeLoadException and use the partial
.Types() list (the successfully-resolved subset), and use a
broader catch (with continue) for any other assembly-level
load failure. The lost user-settings types are not material;
they are derived from ApplicationDbContext in a separate loop
right after, and the RegisterBilling<>() calls that populate
BillingService.Billing run last, after both reflective phases
have completed best-effort.

The test still passes locally because the local test environment
loads a clean set of assemblies; only the CI runner (with its
extra test-time tooling) hits this path.
2026-08-16 16:35:05 +01:00
f92d23b54f
release: 1.0.6
Patch is even (6) and bare, so this is classified as 'stable' by
the validate-release job in .github/workflows/docker-publish-android.yml.

Move the Unreleased section up by inserting [1.0.6] below it, with
a list of changes that landed on this release:

  - Self-hosted Forgejo Actions runner now drives CI on yavsc,
    using pazof/yavsc-build-env:debian12-dotnet10-android36-v1
    pulled from Docker Hub.
  - .forgejo/workflows/buildAndTest.yml builds without
    actions/checkout (image has no Node) and uses NuGet.config
    for the isn.pschneider.fr feed.
  - Dockerfile / Dockerfile.backend drop the redundant
    'dotnet nuget add source' step that broke the APK build on
    GitHub Actions.
2026-08-16 16:18:37 +01:00
d8555a6827
Merge branch 'main' into feat/postit-release-page 2026-08-16 16:07:16 +01:00
4370019785 Merge pull request 'dockerfile: drop inline 'dotnet nuget add source isn.pschneider.fr'' (#18) from fix/github-action-apk into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Successful in 15s
Dotnet build and test / build (push) Failing after 3m49s
Reviewed-on: #18
2026-08-16 15:51:08 +01:00
eaa4c16936
dockerfile: drop inline 'dotnet nuget add source isn.pschneider.fr'
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 16s
Dotnet build and test / build (pull_request) Failing after 7m32s
The project-level NuGet.config (added in 94012c51) lists the isn feed
so 'dotnet restore' picks it up without an inline 'dotnet nuget add
source' step.

The inline add source was duplicating NuGet.config and causing build
failures in GitHub Actions:
  - The --allow-insecure-connections flag did not match the actual
    HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert,
    not self-signed), making the step fail with 'exit code 1'.
  - docker build --target build-env (used by
    .github/workflows/docker-publish-android.yml) hit this on every
    run.

Both Dockerfile and Dockerfile.backend had the same redundant step;
both removed. 'dotnet restore' still finds the feed via NuGet.config
at /src/NuGet.config (copied in by 'COPY . .').
2026-08-16 14:10:10 +01:00
abc507c0f3
dockerfile: drop inline 'dotnet nuget add source isn.pschneider.fr'
The project-level NuGet.config (added in 94012c51) lists the isn feed
so 'dotnet restore' picks it up without an inline 'dotnet nuget add
source' step.

The inline add source was duplicating NuGet.config and causing build
failures in GitHub Actions:
  - The --allow-insecure-connections flag did not match the actual
    HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert,
    not self-signed), making the step fail with 'exit code 1'.
  - docker build --target build-env (used by
    .github/workflows/docker-publish-android.yml) hit this on every
    run.

Both Dockerfile and Dockerfile.backend had the same redundant step;
both removed. 'dotnet restore' still finds the feed via NuGet.config
at /src/NuGet.config (copied in by 'COPY . .').
2026-08-16 14:09:11 +01:00
4862260608 Merge pull request 'forgejo/ci: run build in pazof/yavsc-build-env container' (#17) from feat/postit-release-page into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Successful in 9s
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #17
2026-08-16 13:50:12 +01:00
cc50a8bbc8
ci: unshallow clone for GitVersion
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Successful in 5m41s
GitVersion.MsBuild fails on shallow clones ('Repository is a shallow
clone. Git repositories must contain the full history.') because it
walks the git log to compute the SemVer version.

Drop --depth 1 from both the PR ref fetch and the submodule update
so the runner's working tree has full history. The repo is small
enough that the cost is negligible.
2026-08-16 13:39:53 +01:00
94012c51ab
nuget: add NuGet.config pointing at isn.pschneider.fr
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 14s
Dotnet build and test / build (pull_request) Failing after 1m58s
The yavsc solution depends on HigginsSoft.IdentityServer8.* 8.1.0-alpha.*,
which is only published on the internal feed https://isn.pschneider.fr.
Public nuget.org has 8.0.4 as the nearest version, so every project that
uses IdentityServer8 (Yavsc.Org, Yavsc.Api, Yavsc.Blogs, Yavsc.Server,
cli, Yavsc.Org.Tests, Yavsc.Blogs.Tests) fails with NU1102 on restore.

Both feeds are reachable anonymously, so listing isn first and
nuget.org second in a project-level config restores everything without
credentials. The CI runner on forgejo now sees the same sources as a
local clone.
2026-08-16 13:35:44 +01:00
5f50135c7f
ci: drop working-directory, cd into /src/_src explicitly
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Failing after 2m1s
forgejo-runner v13 does not interpolate ${{ runner.workspace }}
in working-directory: (or ignores the field entirely for docker
containers), so the container tried to chdir to '/_src' (literally)
which does not exist.

The image WORKDIR is /src, so clone directly into /src/_src and cd
into it at the start of each step. Adds an echo of the checkout
SHA + branch state for visibility in the log.
2026-08-16 13:27:36 +01:00
86e59ad1c1
submodule: switch URL from SSH to HTTPS for forgejo anonymous access
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Failing after 13s
The runner container does not have an SSH client, and even if it
did, no key is configured for it. Forgejo Actions must reach the
submodule over HTTPS with anonymous read access (which is now
enabled on the Forgejo instance).

Use 'git submodule sync --recursive' on the developer side after
checkout to propagate the URL change to .git/modules/.
2026-08-16 13:26:12 +01:00
7370f48aac
ci: clone via GITHUB_REF instead of GITHUB_REF_NAME
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Failing after 12s
In pull_request context, GITHUB_REF_NAME is the PR number ('17'),
not the source branch. Cloning --branch 17 fails with
'Could not find remote branch 17 to clone'.

Use GITHUB_REF (refs/pull/N/head in PR context, refs/heads/<branch>
in push context) and fetch + checkout FETCH_HEAD. workflow_dispatch
falls back to the default branch.
2026-08-16 13:22:56 +01:00
380b5d12c8
ci: replace actions/checkout with manual git clone (image has no node)
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Failing after 9s
The pazof/yavsc-build-env:debian12-dotnet10-android36-v1 image only
ships .NET 10 SDK + Android SDK + JDK 17, no Node. actions/checkout@v6
requires Node, so the job failed with 'exec: node not found'.

Replace actions/checkout with a direct git clone over HTTPS (Forgejo
anonymous is enabled), and init submodules recursively.

Also drop the bogus docker://image:tag runs-on: matcher, use just 'docker'
to match the runner's declared label name.
2026-08-16 13:15:10 +01:00
4ac8e14ba9
run on docker
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 9s
Dotnet build and test / build (pull_request) Failing after 2m25s
2026-08-16 13:03:17 +01:00
e165e7bb61
dotnet-android-build-image: pin to <sha-ou-tag>
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 15s
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-16 12:49:50 +01:00
0fe293bcd2
forgejo/ci: run build in pazof/yavsc-build-env container
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 17s
Dotnet build and test / build (pull_request) Has been cancelled
Le workflow buildAndTest tournait sur un runner nu debian-latest avec
setup-dotnet@v5 pour la SDK 10.0.x. Restore échouait car cet
environnement n'a ni la source NuGet interne (isn.pschneider.fr) ni
les workloads Android configurés, contrairement à l'image
pazof/yavsc-build-env utilisée par le Dockerfile.

Bascule le job sur un runner labelisé docker avec l'image
debian12-dotnet10-android36-v1 directement. Le step setup-dotnet
devient inutile (l'image a déjà la SDK 10.0), le restore partage
la même config que le Dockerfile.

Refs l'image cible par ARG BUILD_ENV_TAG=debian12-dotnet10-android36-v1.
2026-08-15 22:18:03 +01:00
a906a96c94 Merge pull request 'postit: validate CHANGELOG section on tag, classify stable/preview/unstable' (#16) from feat/postit-release-page into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Successful in 20s
Dotnet build and test / build (push) Failing after 1m23s
Reviewed-on: #16
2026-08-15 22:02:18 +01:00
93f39ca872
forgejo/ci: pin dotnet sdk 10.0.x for buildAndTest
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 8s
Dotnet build and test / build (pull_request) Failing after 30s
Le repo cible net10.0 partout (csproj, TFM), mais le workflow CI
Forgejo buildAndTest installait une SDK 9.0.x. Aligne sur 10.0.x
pour que la CI build avec une SDK qui connaît le TFM net10.0.

Pas de global.json ajouté : la SDK est résolue à l'installation
de l'image runner, le repo reste agnostique de la version exacte.
2026-08-15 22:00:36 +01:00
2a8c854a4c Merge branch 'main' into feat/postit-release-page
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 8s
Dotnet build and test / build (pull_request) Failing after 32s
2026-08-15 20:35:39 +01:00
99a62ebf81
postit: validate CHANGELOG section on tag, classify stable/preview/unstable
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 8s
Dotnet build and test / build (pull_request) Has been cancelled
Rend le job publish-release dépendant d'un nouveau job validate-release
qui :
- parse le tag (format MAJOR.MINOR.PATCH[-SUFFIX])
- classifie le canal : pair=stable, impair=preview, suffixe=instable
- fail-fast sur instable sauf opt-in explicite via workflow_dispatch
- vérifie que CHANGELOG.md contient une section ## [<tag>] - <canal>
- expose le body de la section via $GITHUB_ENV pour le job de publication

Le tag trigger passe de 'v*' à '*' (pas de préfixe sur les tags), et
le corps de release GitHub est désormais curé via CHANGELOG.md plutôt
que généré automatiquement.

Cette convention de parité est partagée avec le dépôt postit-debian
pour la production des paquets .deb (alignement à traiter dans une PR
séparée).
2026-08-15 20:34:47 +01:00
c24935c94b Merge pull request 'feat/postit-release-page' (#15) from feat/postit-release-page into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Successful in 9s
Dotnet build and test / build (push) Failing after 49s
Reviewed-on: #15
2026-08-15 18:13:05 +01:00
825d54439c
postit: validate CHANGELOG section on tag, classify stable/preview/unstable
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
Rend le job publish-release dépendant d'un nouveau job validate-release
qui :
- parse le tag (format MAJOR.MINOR.PATCH[-SUFFIX])
- classifie le canal : pair=stable, impair=preview, suffixe=instable
- fail-fast sur instable sauf opt-in explicite via workflow_dispatch
- vérifie que CHANGELOG.md contient une section ## [<tag>] - <canal>
- expose le body de la section via $GITHUB_ENV pour le job de publication

Le tag trigger passe de 'v*' à '*' (pas de préfixe sur les tags), et
le corps de release GitHub est désormais curé via CHANGELOG.md plutôt
que généré automatiquement.

Cette convention de parité est partagée avec le dépôt postit-debian
pour la production des paquets .deb (alignement à traiter dans une PR
séparée).
2026-08-15 15:51:42 +01:00
e3987d7890
postit: add CHANGELOG.md with Keep a Changelog format
Initialise le changelog du projet au format Keep a Changelog 1.1.0,
en français, avec une section [Unreleased] vide prête à être curée
au moment de la première release.

Le préambule documente la convention de parité du patch :
- pair → stable
- impair → preview
- suffixe → instable

Cette convention est partagée avec le dépôt postit-debian pour la
production des paquets .deb (alignement à traiter dans une PR séparée).
2026-08-15 15:44:47 +01:00
eebc83cf7e
Add comments API support and tests
Some checks failed
Dotnet build and test / build (push) Has been cancelled
Dotnet build and test / log-the-inputs (push) Has been cancelled
2026-08-10 22:27:52 +01:00
0fd9e40d67
Fix blog comment endpoint path and add regression test
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 21:48:03 +01:00
0d3fbf22c3
GetUserId_reads_NameIdentifier_when_sub_was_mapped
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 18:34:01 +01:00
44b391d496
Activity protection
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 18:12:59 +01:00
cd03b04755
re-refacto BlogPost serialization
Some checks failed
Dotnet build and test / build (push) Has been cancelled
Dotnet build and test / log-the-inputs (push) Has been cancelled
2026-08-05 21:11:22 +01:00
64547840e4
refacto BlogPost serialization
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-05 21:05:14 +01:00
ff7a6ac16d Merge pull request 'publish android' (#13) from build into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #13
2026-08-03 02:36:11 +01:00
c8b05a8950
publish android
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-03 02:35:50 +01:00
42ad1b623d Merge pull request 'build' (#12) from build into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #12
2026-08-03 02:17:32 +01:00
7d1cca9df0
build
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-03 02:16:57 +01:00
3744d9ae9c
Enable blogs on connected status
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-03 01:48:15 +01:00
b25e0e842e
refacto blogPost
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-03 01:36:12 +01:00
1d716380dd Merge pull request 'ci: publish APK to GitHub release on v* tag' (#11) from release into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #11
2026-08-02 23:29:21 +01:00
23262bcc17 ci: publish APK to GitHub release on v* tag
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
Adds a publish-release job that triggers only on tag pushes (refs/tags/v*).
It reuses the APK artifact uploaded by apk-deploy, publishes a GitHub
release via softprops/action-gh-release, and attaches the APK.

Result: a stable permalink to the latest APK at
  https://github.com/<owner>/<repo>/releases/latest/download/PostIt.Android.apk
2026-08-02 23:23:00 +01:00
ead74b7cff Merge pull request 'Le créateur vient de l'authentification, donc on ne le prend pas du post' (#10) from fix/OIDC-debugging into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #10
2026-08-02 23:04:09 +01:00
3b21a12c20 Le créateur vient de l'authentification, donc on ne le prend pas du post
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-02 23:02:54 +01:00
573c12fe30 Merge pull request 'postit: allow self-signed OIDC TLS in Development' (#9) from fix/OIDC-debugging into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #9
2026-08-02 21:32:49 +01:00
286c29f4e3 Navigate to Main Page
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-02 21:08:25 +01:00
786016344b refacto error handling
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 17:56:23 +01:00
fda43ba2d1 Hsts 2026-07-12 16:27:13 +01:00
9622dbeaf2 Hsts 2026-07-12 16:26:43 +01:00
3d80a3f2a1 Post logout redirect uri 2026-07-12 16:14:37 +01:00
13964b9f7f org: show full error details in development
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 16:07:28 +01:00
eba44b46e2 postit: allow self-signed OIDC TLS in Development
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 15:51:55 +01:00
c778f7e46b Merge pull request 'fix/auth-redirect-message' (#8) from fix/auth-redirect-message into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #8
2026-07-12 15:22:20 +01:00
b827f531aa Localisation
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 15:18:07 +01:00
5eb8019beb Audiences 2026-07-12 15:17:53 +01:00
0f73728b85 code format 2026-07-12 15:17:41 +01:00
9f061277c7 auth: fix JWT default scheme and multi-audience validation
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-07-12 14:53:17 +01:00
c354cbab78 Merge pull request 'fic/jwt-validation' (#7) from fic/jwt-validation into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #7
2026-07-12 06:46:05 +01:00
b1e8d37f21 blogs: use centralized JWT audience handling
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 06:45:00 +01:00
1b0933c215 tests: log warning when OIDC token fallback is used 2026-07-12 06:43:07 +01:00
6e4b68c60d Merge remote-tracking branch 'origin/main' into fic/jwt-validation 2026-07-12 06:12:19 +01:00
7fa55a68c9 WIP audiences 2026-07-12 06:10:24 +01:00
bb4ad4fcb8 Merge pull request 'fix/testing' (#6) from fix/testing into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #6
2026-07-12 06:03:39 +01:00
6c100ff759 use an available port for authority
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 06:01:42 +01:00
4aacaf5e51 tests: configure static fixture ports and update org test config 2026-07-12 05:48:47 +01:00
713f66f1a7 ? 2026-07-12 04:36:35 +01:00
09604092c5 Revert "Test host: bind Kestrel to Site:Authority instead of dynamic port"
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
This reverts commit 402bcc1db8.
2026-07-12 03:48:49 +01:00
1ab0bef9a4 using clauses cleanup 2026-07-12 03:48:30 +01:00
402bcc1db8 Test host: bind Kestrel to Site:Authority instead of dynamic port
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
The Yavsc.Org integration tests were failing 'Internal Server Error' on
the OIDC discovery document when run as part of the full test suite.

Root cause: WebHostFixture bound Kestrel to IPAddress.Loopback on a
dynamically-allocated port and exposed it via IServerAddressesFeature.
But the OIDC issuer URLs (and the issuer claim) come from Site:Authority,
which was left at the production value (mercure.pschneider.fr). So
IdentityServer8's discovery document advertised URLs unreachable from
the test process, and the discovery call returned a 500.

Fix:
  - WebServerFixture now overrides Site:Authority and Site:ExternalUrl
    in AddInMemoryCollection to 'https://localhost:44300' (the ASP.NET
    Core dev HTTPS convention).
  - WebHostFixture reads Site:Authority from configuration and binds
    Kestrel to that fixed URL. The exposed Addresses list is sourced
    from the same configuration value instead of the
    IServerAddressesFeature, so the listen URL and the OIDC issuer
    URLs always match.

Remoting.cs (Mandatory/Remoting.cs): add 'using
Microsoft.Extensions.DependencyInjection;' so the existing OIDC/DB
diagnostic block (capture raw HTTP response + dump OIDC-related DB
state on discovery failure) compiles. The diagnostic itself is left
in place — it's what surfaced the 500 in the first place.
2026-07-12 03:43:22 +01:00
becd233593 Merge pull request 'fix/issue-3-splitquery' (#5) from fix/issue-3-splitquery into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #5
2026-07-12 02:47:15 +01:00
f3bb039d2f WIP audiences 2026-07-12 02:42:13 +01:00
9846210fd6 ApplicationDbContext: drop redundant HasOne on 3 Client navs
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
LoadClientAsync(id) on ClientController used to trip an
IndexOutOfRangeException at the InMemory shaper for any
.Include() of one of three Client navs: RedirectUris,
AllowedScopes, AllowedGrantTypes. Five other Client navs (with
the same EF shape and the same application-level config) worked
fine.

Bisection pointed at the InMemory provider; that hypothesis was
wrong. The real cause is in ApplicationDbContext.OnModelCreating:
yavsc was redeclaring the HasOne<Client>().WithMany(...).
HasForeignKey(e => e.ClientId) for all eight Client* navs. The
same relation is already declared (more completely, with
.IsRequired().OnDelete(DeleteBehavior.Cascade)) by
IdentityServer8's ConfigureClientStore via ModelBuilderExtensions.

The redundant mapping on three specific entities — ClientScope,
ClientRedirectUri, ClientGrantType — interacts with the InMemory
provider's shaper in a way that throws IndexOutOfRange. Removing
the redundancy fixes it.

This commit also walks back b12c272d:
- Drops .AsSplitQuery() from LoadClientAsync (no longer needed
  for the InMemory shaper, and the Postgres path it was a hedge
  against was a false alarm — there is no Postgres production
  bug here, only an InMemory shaper quirk surfaced by the
  redundant mapping).
- Removes the 9 Bisect_*_alone tests that were the artefact of
  the provider-hypothesis phase. They pointed at the right
  entities but for the wrong reason.
- Keeps EditRedirectUris_GET_after_add_lists_both_uris as the
  end-to-end regression sentinel: with the fix in place, it
  loads a Client with two RedirectUris and asserts both are
  rendered. Without the fix, it fails with IndexOutOfRange.
2026-07-12 02:39:13 +01:00
b12c272df7 ClientController: split LoadClientAsync into per-collection subqueries
LoadClientAsync chains 9 .Include() calls on dbContext.Clients.
On Postgres (and InMemory for some IdentityServer8 nav types), the
resulting cartesian product trips the query shaper with
IndexOutOfRangeException at IncludeCollection materialisation time.
Bug reproduces in production on the Blog admin pages that load a
Client by id.

AsSplitQuery() rewrites the load as 9 separate SELECTs joined by
client id, which sidesteps the cartesian explosion and any shaper
ambiguity between Claims/Properties/ClientSecrets (which share
Type/Value column names across some IdentityServer8 versions).

Tests:
- EditRedirectUris_GET_after_add_lists_both_uris: end-to-end
  reproducer that adds a second RedirectUri via POST then re-GETs
  the editor. Guards the fix on the integration path.
- Bisect_*_alone: nine unit tests that exercise the same
  .SingleOrDefaultAsync(c => c.Id == id).Include(nav) on the
  InMemory provider, one nav at a time. Pinpointed three
  problematic navs (RedirectUris, AllowedScopes, AllowedGrantTypes)
  on InMemory; kept as a regression net for any future shaper
  regressions on the InMemory provider (not the Postgres path).
2026-07-12 01:15:02 +01:00
cb20b8a2d5 Revert "repoduces the bug"
This reverts commit fa7794b7a0.
2026-07-11 22:17:58 +01:00
Lum
fa7794b7a0 repoduces the bug 2026-07-11 21:56:25 +01:00
7a066707b3 Tests: route Yavsc.Org test host through Testing environment
TestWebApplicationFactory used ASPNETCORE_ENVIRONMENT=Development, which
caused Program.Main's AddConfiguration("org") to load the tracked
appsettings-org.json (the reference file with the
'*** via dotnet user-secrets ou variable d'environnement ***'
placeholder connection string). Npgsql then failed to parse that
placeholder during host startup, failing six integration tests
(observed 2026-07-11: System.ArgumentException on
NpgsqlConnectionStringBuilder.set_Item).

Switching the test host to a dedicated Testing environment makes
AddConfiguration("org") pick up the new optional
appsettings-org.Testing.json file as the last source in the chain
(JSON → env vars), which overrides YavscConnection with the
InMemory marker and the Smtp section with the test stub values.

The .gitignore exception whitelists this file explicitly: it is a
configuration source for the test host, not a secrets file.

The WebServerFixture path is unchanged — it owns its
WebApplicationBuilder and adds the same in-memory override via its
BuildApp hook.
2026-07-11 20:50:39 +01:00
Lum
cb7526de9d Blog: add test guarding the display-template fix + document test architecture
Le commit 2 a fixé la NPE du /BlogSpot/Details/{id} en passant
le display template par UserDisplayHelpers.AvatarSrc, qui
défend contre un UserName null. Ce commit complète le filet
de non-régression et pose la doc d'architecture des tests.

- ApplicationUserDisplayTemplateTests : assert que le cshtml ne
  concatène plus directement Model.UserName (ancien code fautif)
  et qu'il utilise bien le helper. Si quelqu'un revert la ligne
  4 du cshtml, les tests cassent. Les autres usages de
  Model.UserName (alt, title, asp-route-id) sont autorisés : ils
  ne sont pas la cause du 500, juste laids si null.
- doc/testing.md : vue d'ensemble de la stratégie de test
  (conventions NonRegression/Mandatory/Smoke/Controllers, EF
  in-memory via InMemoryDatabaseRoot partagé, auth stubs,
  quand ne pas écrire de test).
- src/Yavsc.Tests.Shared/README.md : détails du scaffold partagé
  (WebHostFixture + son cycle de vie et ses hooks,
  TestAuthPolicyProvider, TestTokenIssuer) et des deux
  spécialisations dans le repo
  (Yavsc.Org.Tests.WebServerFixture et
  Yavsc.Blogs.Tests.BlogsWebServerFixture).
- doc/README.md : entrée vers testing.md dans l'index.
2026-07-11 19:59:03 +01:00
Lum
bbdcc7f2ad Blog: render user avatar through a null-safe helper
GET /BlogSpot/Details/1 retournait 500 (avec un 500-sur-500 sur
la page d'erreur elle-même) parce que DisplayTemplates
/ApplicationUser.cshtml faisait `var avuri = "/Avatars/" +
Model.UserName + ".s.png"` : avec <Nullable>enable</Nullable>,
Razor émet un null-check implicite sur Model.UserName et lève
NullReferenceException quand l'auteur n'a pas de UserName
posé (donnée héritée, user partiellement initialisé).

- UserDisplayHelpers.AvatarSrc : helper statique pur dans
  Yavsc.Abstract.Identity qui retourne
  YavscConstants.DefaultAvatar pour user null / UserName vide
  ou whitespace, et un path /avatars/<name>.s.png sinon.
- ApplicationUser.cshtml : utilise le helper.
- Tests : 4 cas (null, vide, whitespace, valide) dans
  Yavsc.Org.Tests/NonRegression.

Bonus : le path d'avatar passe de "/Avatars/" (S majuscule,
ne résolvait pas dans le middleware de fichiers statiques) à
YavscConstants.AvatarsPath ("/avatars" minuscule), pour fermer
l'autre trou que centraliser le calcul permettait de fixer
proprement.
2026-07-11 19:41:09 +01:00
Lum
98613e7070 Blog: enforce Restrict FK on BlogPost.Author and Comment.Author
Tout billet a un auteur, tout commentaire a un auteur. On aligne la
base sur ce contrat (Postgres) en droppant les orphelins existants
puis en remplaçant les FK en cascade par des FK Restrict.

- ApplicationDbContext: fluent pour BlogPost.Author et Comment.Author
  en DeleteBehavior.Restrict.
- ApplicationUser: ajoute la nav inverse BlogComments (manquait,
  EF aurait sinon créé une shadow FK).
- Migration 20260711173717_EnforceBlogAuthorFKs: Up purge les
  Comment/BlogSpot dont l'AuthorId n'existe plus, log le volume,
  puis drop+add des FK. Down laisse la cascade (état pré-migration).

Le code applicatif (BlogSpotService.Details) s'appuiera sur cette
contrainte dans un commit séparé.
2026-07-11 18:45:27 +01:00
7f56afd7e0 Merge commit 'bed9c8a272'
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-07-11 03:36:31 +01:00
bed9c8a272 fixes the compile and timestamps to db 2026-07-11 03:26:13 +01:00
8e8c558295 Merge pull request 'postIt' (#2) from postIt into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #2
2026-07-11 02:57:28 +01:00
abfc68a809 Just post one, at least
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-11 02:56:14 +01:00
03cd9843d3 PostIt/Yavsc.Blogs: surface 4xx body + pin controller + red UI test for Save
The 'Save' button in PostIt has been returning 400 from
/api/v1/blog ever since the editor's title and article fields
were re-bound to SelectedPost.Title / SelectedPost.Article.
The user types into the editor, taps Save, the controller
rejects with 'The Title field is required', and the PostIt
status bar shows only the generic 'Response status code does
not indicate success: 400' — no field name, no reason.

Three pieces here make the regression diagnosable and pin a
test for the fix:

1. YavscApiClient: replace EnsureSuccessStatusCode() at both
   call sites with a small helper that reads the response
   body and embeds it in the thrown HttpRequestException. The
   VM's existing catch (Exception) in ExecuteAsync forwards
   ex.Message to the status bar, so the next 'click Save'
   tells the user exactly which field the server rejected.

2. Yavsc.Blogs.Tests: two integration tests on the real
   controller (no HTTP mock) — one pins that a well-formed
   PostIt-shaped payload (Title + Article + AuthorId + dates,
   Id=0) is accepted with 201, the other pins that a payload
   with Title=string.Empty is rejected with 400. Together they
   pin the contract the VM has to honour.

3. PostIt.Tests: a red [AvaloniaFact] UI test that mounts
   MainPage inside a headless Window, types a title into the
   TextBox without first selecting a post in the list, taps
   Save, and asserts the body of the first POST contains
   the typed title. Today this test fails with Title='',
   reproducing the production 400. The matching fix (a
   Title/Article buffer on MainPageViewModel that the XAML
   binds to, and that Save uses to build the outgoing
   BlogPost) is the next commit; the test is the safety net.
2026-07-11 02:52:50 +01:00
56846e2781 Log the identity main params 2026-07-10 00:27:18 +01:00
58c08fe2fc auto refresh token 2026-07-09 23:34:15 +01:00
fdb7acca56 using Material.Avalonia 2026-07-09 23:28:46 +01:00
5104ffeb81 postIt: wire DarkMode, drop dead themeVariant, add UI tests for the banner
Three related changes that close the loop on the DarkMode
field and lay the first stone of a UI test scaffold for
PostIt.

1. Settings.DarkMode was previously a dead field. It round-
   tripped through postit-settings.json and the SettingsPage
   CheckBox, OnDarkModeChanged flipped IsDirty, and that was
   it — no consumer ever read the value, so toggling the
   CheckBox had no visible effect. The fix is in
   App.OnFrameworkInitializationCompleted: read the value
   Load() just populated and set
   Application.Current.RequestedThemeVariant accordingly
   (so a dark-mode user lands on a dark window on first
   launch, not on a default-light window that flips after
   the user touches the toggle), then subscribe to
   settings.PropertyChanged and update the theme on every
   DarkMode change. The consumer lives in App.axaml.cs, not
   in Settings, so the Settings model stays free of any
   Avalonia.Application dependency and the SettingsLoadTests
   (which construct Settings outside an Avalonia host)
   still pass unchanged.

2. MainPageViewModel had a vestigial [ObservableProperty]
   ThemeVariant themeVariant = ThemeVariant.Default that no
   XAML, no code, and no test ever read. It was the start of
   a half-finished attempt to expose the theme variant on
   the page VM. The dark-mode wiring above makes it
   irrelevant: the theme is now driven by Application, not
   by a VM property. The field is removed, along with the
   using Avalonia.Styling; it pulled in (now unused).

3. SessionStatusBannerTests adds the first set of UI tests
   for PostIt. They mount a real MainWindow via the headless
   Avalonia host declared in TestApp.cs, attach a
   SessionStatusViewModel as the banner's DataContext, and
   assert the actual visual tree contents: three buttons
   render (Se déconnecter, Se connecter, Paramètres), the
   Login button is visible when logged out, the Logout
   button is hidden when logged out, the Paramètres button
   is visible regardless of session, and the session label
   text reflects the VM. The pattern follows what
   UnitTest1.MainPage_Should_Load already established:
   [AvaloniaFact] (from Avalonia.Headless.XUnit) plus
   new MainWindow() / window.Show(). A plain [Fact] cannot
   drive Window..ctor() because the headless platform's
   PlatformManager.CreateWindow() has no service registered
   outside a dispatcher-aware test context; the
   AvaloniaFact attribute provides that context. The
   DataContext is set on the banner directly because
   App.OnFrameworkInitializationCompleted is not called in
   a unit test (production wiring is exercised by the
   manual launch, not here).

Build: 0 errors. Tests: 5/5 SessionStatusBannerTests,
3/3 SettingsLoadTests, 1/1 MainPageTests (the existing
scaffold test, unchanged). The other PostIt.Tests suites
depend on the OIDC stub WebApplicationFactory and time out
on this network-restricted host.
2026-07-09 22:41:43 +01:00
1733dababb postIt: SettingsPage is a singleton, navigation is idempotent
Two related changes that close the loop on the SettingsPage
push semantics.

1. The SettingsPage used to be registered as Transient. Each
   click on the Paramètres button resolved a fresh instance,
   re-bound it to the Settings singleton, and pushed it onto
   the navigation stack. Repeated clicks accumulated stacked
   instances, each fully bound, and the user had to tap Back
   N times to leave. The fix is to register the page as a
   Singleton in the DI container. There is now one and only
   one SettingsPage ContentPage for the lifetime of the app:
     - its DataContext is wired once, at composition time
       (just after the ViewLocator is added to DataTemplates),
       not on every push;
     - the OpenSettingsRequested handler is a pure navigation
       concern, with no DI resolution and no rebinding;
     - the in-memory Settings state is preserved across visits
       (any in-flight edit stays in the same instance).

2. The OpenSettingsRequested handler is guarded so that if the
   SettingsPage is already at the top of NavigationStack, the
   push is a no-op. NavigationPage.PushAsync does not
   deduplicate; without the guard, calling it twice with the
   same instance pushes it a second time, and the user has to
   tap Back twice to leave. The guard is a reference comparison
   on NavigationStack[Count - 1] against the singleton
   instance, which is correct precisely because the page is
   a singleton.

doc/architecture/postit.md is updated to match: the DI table
reflects the new lifetime, and the 'Garde anti-empilement'
section is rewritten from 'to be implemented' to the actual
implementation, including the rationale for reference
comparison and the cross-dependency between the singleton
lifetime and the guard.

The Settings-singleton invariant (in the same doc) is
unchanged: Settings is still a singleton, and adding a
transient override would still be the bug it always was.
The new SettingsPage singleton sits alongside it cleanly.

Build: 0 errors. Tests: 3/3 SettingsLoadTests green.
2026-07-09 22:00:00 +01:00
120bae6f5c doc(arch): PostIt topology + up-to-date project list
Two long-standing gaps in the architecture documentation are
filled in this commit:

1. doc/architecture/postit.md is new. It covers everything the
   existing postit-oidc.md does not: the one-codebase /
   three-frontends topology (PostIt lib + PostIt.Desktop +
   PostIt.Android + PostIt.Browser), the custom ViewLocator
   that resolves ViewModel -> View through the DI provider
   (and why we don't use the Avalonia.Mvvm default), the
   composition root in App.OnFrameworkInitializationCompleted
   with the full DI registration table, the navigation flow
   driven by SessionStatusViewModel events, the ViewModel
   lifetime conventions (singleton vs transient), the
   [RelayCommand] XAML binding conventions (referenced to
   AGENTS.md for the canonical version), and the per-page
   DataContext / role table. The Settings-singleton invariant
   is called out as a guard rail, and the SettingsPage
   anti-empilement invariant is documented as the TODO the
   code still owes us.

2. doc/architecture/decoupage-organisation.md is brought up to
   date. Its project table listed 7 .csproj; the repo has 14
   (the four PostIt projects, the tests satellites, the cli
   tool). The table is extended, the ASCII diagram picks up
   the PostIt block, and an Outils et tests section lists
   the test / CLI satellites that were missing.

doc/README.md is updated to index the new postit.md. No code
changes in this commit, no behaviour change.
2026-07-09 21:54:06 +01:00
13d985e4e3 fixes the button access 2026-07-09 21:35:06 +01:00
d3664c5cdc postIt: scope list in SettingsPage, fix Settings DI re-registration
Two changes to the PostIt settings surface, both in service of
the same observation: opening the Settings page did not reflect
the loaded state, and edits to Authority / ClientId did not
persist.

1. Settings was registered twice in the DI container: once as
   a singleton (the already-Load()'d instance) and again as a
   transient, with the transient registration winning. The
   Settings page's DataContext was therefore a brand-new,
   empty Settings instance on every push — Authority and
   ClientId bound to null, and even if the user typed into the
   fields, the edits landed on the throwaway instance and were
   silently lost. The fix is the obvious one: keep Settings as
   a singleton and drop the transient override.

2. The Scopes field of AuthenticationSettings is a string[],
   which doesn't bind to a TextBox without a converter. The
   Settings page already shows the other auth fields as plain
   TextBoxes, so the same treatment is given to scopes via a
   new space-separated view property:

     - AuthenticationSettings.ScopeListText (string,
       [ObservableProperty], [JsonIgnore]) is the view.
     - OnScopeListTextChanged splits on any whitespace and
       re-assigns Scopes, skipping the write when the parsed
       array is element-wise equal to the current one to avoid
       a PropertyChanged loop with OnScopesChanged.
     - OnScopesChanged keeps ScopeListText in sync when
       Scopes is reassigned from outside (JSON hydration,
       MergeScopes, programmatic updates), again short-
       circuiting when the textual representation hasn't
       changed so the TextBox caret doesn't flicker on load.
     - RefreshScopeListText is the explicit re-sync entry
       point; Settings.ApplyJson calls it after a successful
       hydration to normalise any whitespace the JSON might
       have introduced.

   SettingsPage.axaml gets a new Scopes row between ClientId
   and the Blogs API URL; the Grid.RowDefinitions are bumped
   to 13 to match. Scopes remains the on-disk format — only
   ScopeListText is presentation.

   The shape of the on-disk postit-settings.json is
   unchanged: [JsonIgnore] on ScopeListText, and the
   serialization path in Settings still round-trips Scopes
   directly. MergeScopes in Settings.GetOidcClientOptions is
   untouched.

Tests: 3/3 SettingsLoadTests passing (PostIt.Tests);
PostIt.csproj builds clean (0 errors). The other PostIt.Tests
suites depend on the OIDC stub WebApplicationFactory and
time out on this network-restricted host, so we trust the
unit-level coverage and the build.
2026-07-09 21:24:48 +01:00
375e6482a6 test(yavsc.org): cover the kid derivation in ComputeKid
Extract the kid calculation out of LoadSigningCredentialsInner
into a new internal static HostingExtensions.ComputeKid(string),
and cover it with five focused unit tests in
Yavsc.Org.Tests.ComputeKidTests.

The kid is the bit of signing-credential metadata that ties a
JWT to the right key in the JWKS. Without it, resource servers
(Yavsc.Blogs, Yavsc.Api) fail signature validation with IDX10500
'The signature key was not found', as fixed in 2c6d1157. That fix
inlined three lines of thumbprint-truncation logic at the top of
LoadSigningCredentialsInner, but left the calculation untested.
The tests in this commit pin its shape, value, stability, and
uniqueness, so a future refactor (e.g. switching from SHA-1 to
SHA-256, or moving to X509CertificateLoader for SYSLIB0057) has
to update them deliberately instead of silently changing the
JWKS key id.

Concretely:

  - InternalsVisibleTo("Yavsc.Org.Tests") in AssemblyInfo.cs
    gives the test project access to the new internal method
    without forcing LoadSigningCredentialsInner to leak
    further.
  - ComputeKid(string) is the single source of truth for the
    16-hex truncation; the production call site in
    LoadSigningCredentialsInner now reads
    'var kid = ComputeKid(certPath);'.
  - The inline comment block is updated to say SHA-1 (which is
    what X509Certificate2.GetCertHash() actually returns) instead
    of the previous SHA-256 claim. The behaviour is unchanged.
  - ComputeKid uses X509CertificateLoader.LoadCertificateFromFile
    rather than the obsolete 'new X509Certificate2(string)' ctor
    (SYSLIB0057); same on-disk behaviour, no obsolete warning.

Tests cover:
  - 16-char upper-case hex output matching the first 16 hex
    chars of the cert's GetCertHash();
  - stability across repeated reads of the same cert;
  - distinctness between two independently generated certs;
  - the SHA-1 size of the underlying thumbprint (20 bytes), so
    a future switch to SHA-256 forces a test update;
  - CryptographicException propagation for a missing cert file
    (Assert.ThrowsAny to stay portable across the Linux OpenSSL
    and Windows leaf exception types).
2026-07-09 20:28:58 +01:00
2c6d11577c Yavsc.Org: set KeyId on signing credentials
IdentityServer8 was emitting JWTs without a 'kid' header and
serving the JWKS without per-key identifiers, because
LoadSigningCredentialsInner constructed RsaSecurityKey /
ECDsaSecurityKey objects without an explicit KeyId. Resource
servers (Yavsc.Blogs, Yavsc.Api) cannot match a token to a key
in the JWKS without one, so every signature validation failed
with 'The signature key was not found' (Microsoft.IdentityModel
IDX10500).

Root cause: SigningCredentials were built directly from the
BC-parsed key parameters, bypassing the X509Certificate2 path
IdentityServer normally derives the kid from. The fix derives
a stable KeyId from the certificate's SHA-256 thumbprint
(truncated to 16 hex chars) and sets it on both SecurityKey
variants before constructing SigningCredentials.

The thumbprint-based kid is stable across process restarts as
long as the cert doesn't change, and changes naturally on
LetsEncrypt renewal (~90 days), which is the right behaviour:
old tokens age out, resource servers refresh their JWKS cache
to discover the new kid.

Production rollout: redeploy Yavsc.Org and re-login (or let
the refresh-token path rotate) so newly issued tokens carry
the kid. Pre-restart tokens will continue to be rejected with
IDX10500 until they expire or are refreshed.
2026-07-09 00:32:20 +01:00
6055117929 first, the blogs 2026-07-08 23:18:14 +01:00
0adc60d5ed simpler 2026-07-08 23:16:16 +01:00
2eadd6e1b7 better 2026-07-08 23:16:04 +01:00
b7873ebd7c GET posts [dev] 200 2026-07-08 22:46:46 +01:00
0aea6c0dbd PostIt: Sauver button on SettingsPage with dirty tracking
SettingsPage.axaml had TextBox / CheckBox TwoWay bindings to the
Settings singleton, but no Save button — user edits mutated the
in-memory instance and were lost on the next launch. This commit
addes the missing save path:

- Settings.Save() writes the current instance to
  ~/.config/PostIt/postit-settings.json (symmetrical to Load),
  with 0600 POSIX permissions matching TokenStore.Save.
- Settings.IsDirty ObservableProperty flips to true on every
  setter that flows through the four top-level
  [ObservableProperty] fields (DarkMode, BlogsApiUrl,
  BusinessApiUrl, plus the OnAuthenticationChanged partial for
  the Authentication sub-property). Sub-property edits
  (Authentication.Authority / ClientId / RedirectUri / Scopes)
  are caught by a PropertyChanged subscription wired up in
  OnAuthenticationChanged and re-wired on each Authentication
  reassignment.
- [RelayCommand(CanExecute = nameof(CanSave))] on Save itself
  emits the SaveCommand ICommand that the XAML binds to.
  OnIsDirtyChanged calls SaveCommand.NotifyCanExecuteChanged()
  so the button auto-enables / auto-disables. The Avalonia
  binding is 'SaveCommand' without a suffix — the source
  generator emits that property name from the Save method.
- ApplyJson resets IsDirty = false at the end so disk / embedded
  loads don't leave the page stuck in dirty state.
- SettingsPage.axaml: fixed the RowDefinition count (4 rows
  declared, 10 used — controls at rows 4..9 were rendering
  outside the grid), and added a Sauver button at row 10 bound
  to SaveCommand with IsEnabled driven by !IsDirty.

Build: dotnet build src/PostIt/PostIt/PostIt.csproj → 0 errors.
Tests: 45 / 45 passing.
2026-07-08 20:05:09 +01:00
e9df13a477 test(postit): pin blogs scope on bearer, fix post-refactor tests
Two intertwined jobs here:

1. Diagnostic test for the production 401 we see when PostIt
   talks to Yavsc.Blogs. The hypothesis this test isolates:
   the access token sent on the wire is missing the 'blogs'
   scope that Yavsc.Blogs' BlogScope policy requires (see
   Yavsc.Blogs/Program.cs: RequireClaim(JwtClaimTypes.Scope,
   "blogs")). The test fakes a single HttpMessageHandler,
   captures the outbound bearer, decodes the JWT, and asserts
   the 'scope' claim contains 'blogs'. It does not stand up a
   server, an OIDC stub, or any network listener. Result: the
   scope is present in the access_token we construct, so the
   401 is not on the client side — most likely the IdP at
   Yavsc.Org is not issuing 'blogs' as a recognised scope.

2. Mechanical fix of the three test files that broke during
   the Settings model refactor (PostIt.Settings ->
   PostIt.ViewModels.Settings; ApiUrl -> BusinessApiUrl;
   Scopes/RedirectUri moved under Authentication;
   DefaultDesktopRedirectUri is on AuthenticationSettings in
   the global namespace). Also restored the BaseAddress
   setup that BlogApiClient does in production in
   LoginAndPersistAsync / the reloaded-client path of
   YavscApiClientTests, so the two integration tests that
   call CallAsync("posts") directly don't trip on
   'request URI must be absolute or BaseAddress must be set'.

Test status: 45 / 45 passing in PostIt.Tests.
2026-07-08 19:36:40 +01:00
4a6609e2f1 PostIt: route Paramètres button to SettingsPage
The "Paramètres" button on SessionStatusBanner was wired to a stub
OpenSettingsCommand with a TODO. With the Settings model refactor
(VM consolidated to ViewModels/Settings.cs, SettingsViewModel.cs
dropped, App.axaml.cs registering Settings instead of the old VM),
the navigation is now plumbed end to end:

- SessionStatusViewModel gains an OpenSettingsRequested event
  alongside LogoutCompleted / LoginSucceeded, and the
  [RelayCommand] body just raises it. VM stays decoupled from
  NavigationPage and window lifetime, same pattern as the
  existing banner events.
- App.axaml.cs handles the event in the desktop branch: resolves
  SettingsPage (transient) and the canonical Settings singleton
  (the one we Load()'d at startup and bound via
  Settings.BindToServiceProvider) from DI, then PushAsync the
  page on top of the current NavRoot stack. Two-way bindings on
  SettingsPage mutate the singleton in place.

Build: dotnet build src/PostIt/PostIt/PostIt.csproj → 0 errors.
Existing CS8602 / NU1507 / CS8632 warnings unchanged.
2026-07-08 19:03:54 +01:00
0480e3e3e8 fix(blogs): drop debug MapGet("/identity") claim-dumper
The endpoint was a copy-paste from the IdentityServer template
documentation. It serialised the entire HttpContext.User claim set
to anonymous JSON, with no auth gate. In a public-facing deployment
that's exactly the kind of surface scrapers and botnets love
(it tells them whether their token is valid and what shape the
issuer uses), and it served no production purpose.

Side benefit: removes the ASP0004 analyser warning ("IActionResult
should not be returned from a MapGet Delegate") that came with
this line.
2026-07-07 22:13:29 +01:00
006e05a375 fix(blogs): register controller endpoints via MapControllers
All [ApiController] classes (BlogApi, BlogTags, PostTags, FileSystem,
FileSystemStream, Comments, TagsApi) returned 404 on every route,
even though Kestrel was up. The pipeline in Program.cs was missing
MapControllers(), so the controllers were never attached to the
endpoint data source. MapIdentityApi and MapGet("/identity")
worked because they're explicit minimal-API routes; the attribute-
routed controllers didn't.

Confirmed end-to-end: GET /api/v1/blog now returns 401 (auth
required by [Authorize("BlogScope")]) instead of 404.
2026-07-07 21:48:02 +01:00
4dc1946c82 PostIt: drop unused Settings.folder + align ViewLocator nullability
Two leftover bits of dead code that were just compiler noise:

  - Settings.folder (IStorageFolder?, never read) plus the three
    Avalonia / Avalonia.Platform.Storage usings that only existed
    to type it. The picker-based flow was replaced by a direct
    file-path read in Settings.Load, so the field has been a
    CS0414 for a while. Just delete it.

  - ViewLocator.Build / Match took 'object data' while the
    IDataTemplate interface expects 'object? data', which is why
    the compiler was complaining with CS8767 about nullability
    mismatch on every implementation. Add an explicit null arm
    in the switch so the default branch doesn't have to
    dereference a possibly-null data either.
2026-07-07 20:59:26 +01:00
9e272a8147 PostIt: drop LoginPage, hoist Login into session banner, drop AuthorId field
Login flow no longer needs a dedicated page. The OIDC interactive
login now lives on the persistent SessionStatusBanner, alongside
'Se déconnecter', driven by a new SessionStatusViewModel.LoginAsync
command. On success the VM raises LoginSucceeded and App.axaml.cs
pushes MainPage on top of HomePage — same path BootAsync already
takes when the silent refresh succeeds at boot, so the two flows
can't drift apart (PushMainPageAsync helper, single source of
truth).

MainPage no longer shows an editable AuthorId field: the server
infers the author from the bearer token, so the client-side
control was misleading at best. The detail grid drops from 5 rows
to 4.

Removed:
  - Views/LoginPage.axaml + .axaml.cs
  - ViewModels/LoginPageViewModel.cs
  - HomePage Login button + OnLoginClick code-behind
  - DI registrations for LoginPage / LoginPageViewModel
  - ViewLocator mapping
  - dangling <c>LoginPage*</c> cref / comments in Platform.cs,
    PlatformBootstrap.cs (Desktop + Android), MainWindow.axaml,
    YavscApiClient.cs
2026-07-07 20:47:00 +01:00
c3f2408c4a test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.

Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.

Notes for future-me:
  - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
    called once on the first Issue() to keep the 'sub' claim
    literal; without it UserHelpers.GetUserId (which reads 'sub')
    gets ClaimTypes.NameIdentifier instead, returns null, and the
    owner check fails for every PUT. The companion
    options.MapInboundClaims = false on the validation pipeline
    keeps both sides in sync.
  - Production still uses AddYavscJwtBearer against the OIDC
    authority; the test-only HS256 path is local to the test
    process and never crosses a network boundary.

Coverage:
  - GetBlog_returns_401_when_no_token_is_provided — anonymous
    request, real policy fails closed.
  - PutBlog_with_valid_token_and_owner_returns_204_and_Get_
    reflects_update — POST then PUT then GET, all behind a real
    JWT, asserting 204 + list contains the updated title.

Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
6d222cf819 fix(blogs): accept JSON on POST /api/v1/blog (no file)
BlogApiController.PostBlog called Request.Form.Files
unconditionally, which throws on a plain JSON body — the
exception is "This request does not have a Content-Type header.
Forms are available from requests with bodies like POSTs and a
form Content-Type of either application/x-www-form-urlencoded or
multipart/form-data."

That broke PostIt's first-bill-of-blog flow: the client posts a
BlogPost as JSON and has no files to attach. The endpoint
contract is [FromBody] BlogPost, so the JSON body is deserialised
into 'blog' as expected — only the IFormFileCollection argument
to BlogSpotService.Create needs a real-or-empty value.

Branch on Request.HasFormContentType: pass the form files when
present, pass an empty FormFileCollection otherwise. BlogSpotService
already short-circuits on a null/empty file collection, so the
JSON-only path is now a clean code path.

Tests: add PostBlog_creates_a_post_and_Get_returns_it_in_the_list
(POST a draft, assert 201 + server-assigned Id, GET the index,
assert exactly one entry with that Id). Add a per-test
ResetDatabase helper because the in-memory store is shared across
the lifetime of the BlogsWebServerFixture instance.
2026-07-06 22:03:41 +01:00
9f5a1505e3 test(blogs): GET /api/v1/blog returns 200 with empty list
First behavioural test for the Yavsc.Blogs API surface. Sends a
GET on the blog index with the X-Test-Role auth bypass and asserts
the response is 200 with an empty JSON array — the in-memory
ApplicationDbContext has no rows, and BlogSpotService.Index returns
an empty enumeration.

While here, fix a routing miss: AddControllers() in the test
fixture was only scanning the test assembly, so BlogApiController
was never registered. Add the Yavsc.Blogs application part
explicitly. Without this, every request to /api/v1/blog came back
as 404 — the same symptom PostIt was seeing in production.

The POST flow lands in the next commit, once BlogApiController is
made to accept JSON (it currently requires multipart/form-data
because of Request.Form.Files).
2026-07-06 21:58:14 +01:00
8c38bab45a feat(tests): scaffold Yavsc.Blogs.Tests with BlogsWebServerFixture
Adds the test project that the next commit will use to assert the
blog API endpoints. The fixture inherits from the shared
WebHostFixture (commit "refactor: extract WebHostFixture…") and
wires up only the bits the blog API needs:

* In-memory ApplicationDbContext — BlogSpotService is used as-is,
  no mock. The first tests will exercise the real service against
  an empty table.
* Trivial IFileSystemAuthManager stub (the GET index path never
  reads the file system).
* TestAuthPolicyProvider swapped in, so X-Test-Role satisfies
  [Authorize("BlogScope")].

Two smoke tests verify the fixture boots and the controller
pipeline is reachable. The first behavioural test
(GET /api/v1/blog → 200) lands in the next commit.

Also promotes two xunit.v3.* package versions to the root
Directory.Packages.props so future test projects can share them.
2026-07-06 21:49:53 +01:00
49b9619af7 Merge pull request 'postIt' (#1) from postIt into main
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
Reviewed-on: #1
2026-07-06 21:35:47 +01:00
349ddc03f5 refactor: extract WebHostFixture + TestAuthPolicyProvider to shared lib
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
Yavsc.Blogs.Tests will need a fixture too. Lifting the cross-cutting
plumbing (Kestrel + self-signed cert + address discovery + lazy init)
into a new Yavsc.Tests.Shared project lets the next fixture inherit
from it without copying 200+ lines of setup boilerplate, and keeps
the Org.Tests fixture focused on its IdentityServer + SMTP seed.

* New project src/Yavsc.Tests.Shared with WebHostFixture (abstract)
  and TestAuthPolicyProvider (test auth bypass via X-Test-Role).
* WebServerFixture in Org.Tests now inherits from WebHostFixture;
  BuildApp + ConfigurePipelineAsync hold only Org-specific work.
* Two shared package versions promoted to the root Directory.Packages.props.
* Tests still 30/30 green.
2026-07-06 21:33:57 +01:00
20a6f22ec3 PostIt: document the BaseAddress / pathPrefix URL convention
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
The previous "PostIt: fix blog API double-prefix" commit changed
DefaultPathPrefix from "api/blog" to "blog" without spelling out
the convention. Future-me (or anyone else touching ApiUrl) needs
to know that BaseAddress already terminates in /api/v1/ and that
pathPrefix is relative to that.

* BlogApiClient: add a <para> in the class summary that names the
  convention, points at the matching controller route, and
  cross-references the fix commit.
* postit-oidc.md: add a row in the "Composants partagés" table
  with the same warning, in the architectural-doc voice.
2026-07-06 21:03:10 +01:00
0d2d4160af 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
f2ae01729a clean up
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-07-06 19:19:28 +01:00
dce17888ac code cleanup 2026-07-06 03:31:05 +01:00
f11913ec08 build 2026-07-06 03:26:57 +01:00
a5acfcfc05 revert 2026-07-06 03:25:19 +01:00
89aa2bc37d migration 2026-07-06 03:17:23 +01:00
4a70abc0f9 Merge branch 'feat/estimate' 2026-07-06 01:07:43 +01:00
835cb47b18 could fix the CI 2026-07-06 01:06:50 +01:00
aca3ceffe2
Merge pull request #66 from pazof/feat/estimate
Feat/estimate
2026-07-06 00:58:04 +01:00
19e3b30830
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
3932d8823f
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
6ac264fa2c tests 2026-07-06 00:47:35 +01:00
c08ff81776 fixes the startup 2026-07-06 00:14:22 +01:00
333b066e66 Identity reloaded 2026-07-05 23:56:10 +01:00
dependabot[bot]
a2c7cf9f9c
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
ef0f5ddcac refacto FrontmatterParser 2026-07-04 22:58:29 +01:00
7b3a236bcc refacto query status 2026-07-04 22:35:53 +01:00
ee6cb34c23 renaming Reviewed 2026-07-04 22:25:59 +01:00
c74ac71b5d layouts 2026-07-04 20:09:58 +01:00
bce6280750 titres 2026-07-04 20:03:56 +01:00
c8894c4220 gives titles 2026-07-04 19:57:03 +01:00
34b4203cd1 Ui fixes 2026-07-04 19:49:48 +01:00
17838bc78e fixes 2026-07-04 18:46:24 +01:00
a0342ea988 Revert "search all user by email at forgotten password"
This reverts commit 406e2ff03a.
2026-07-04 17:29:49 +01:00
406e2ff03a search all user by email at forgotten password 2026-07-04 17:24:12 +01:00
742da7c3f0 Activity moderated 2026-07-04 17:11:43 +01:00
90dfe9c13f drop the deigner.cs 2026-07-04 16:06:02 +01:00
Lum
f4eb14d083 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
1d26cbdf3d refacto chathub 2026-07-04 15:28:07 +01:00
Lum
b939c403f6 feat(postit): signature capture page (dev entry, file persistence)
Builds on b0495514 (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 b0495514 landed. It is
  folded into this commit rather than amending b0495514 to keep
  the existing history readable; the change is mechanical and
  tested by the new SignaturePageViewModelTests.
2026-07-04 15:11:22 +01:00
b049551448 a Signature Pad 2026-07-04 14:43:39 +01:00
ec54108cf0 presentation 2026-07-01 00:13:57 +01:00
0cc82fec5d bug fix IUserSettings 2026-06-30 23:55:43 +01:00
0965caaf40 fixes and renaming 2026-06-30 23:32:05 +01:00
1751145be8 Exploiting GitVersion 2026-06-28 14:11:26 +01:00
Lum
0617fc6bda 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
bd3dba0089 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
2f7df74000 petit refacto 2026-06-28 01:43:39 +01:00
fd6d6ab2d8 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
4e8402e766 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
4566223a2c tests: feed WebServerFixture a Smtp config so Authenticate fires
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
87e824fa63 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
aaf71bf91c 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
b56277c153 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
e054e58e26 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
5f5003d125 CodeQL 2026-06-27 18:37:18 +01:00
61501e06a3
Create codeql.yml 2026-06-27 18:30:07 +01:00
9fd928a3e2
Create SECURITY.md 2026-06-27 18:28:42 +01:00
4b4afa2eae 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
831b5139c1 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
7069faa1fd roadmap: tick off conteneurisation item in Jalon 0
The 14 docker commits landed across this session achieve the
critère de sortie: 'docker compose up' starts db/api/blogs on
a bare host, and web fails with a documented IdentityServer
signing-certificate error that points at the installation
procedure (volume mount + Kestrel:Endpoints:Https in
appsettings-org.json).

Update the checkbox status and link to CONTRIBUTING.md so a
reader of ROADMAP.md can navigate to the install procedure.

Two Jalon 0 items remain open:
- ◐ NuGet centralisation (partial)
- ☐ Tests d'intégration smoke par BC
2026-06-27 18:17:18 +01:00
c9c71ec645 Merge remote-tracking branch 'github/dependabot/github_actions/all-actions-640176b5ab' 2026-06-27 18:13:33 +01:00
dependabot[bot]
50fbb62f92
Bump actions/checkout from 6 to 7 in the all-actions group
Bumps the all-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 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/v6...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-06-21 16:04:00 +00:00