Commit graph

2 commits

Author SHA1 Message Date
5e3d361f88
feat(acl): server endpoints + client + UI for circle membership
Round-trip out a long-standing hole in the ACL feature: until
this commit a circle on Yavsc was an empty named bucket. You
could create 'Famille' and grant it on a post, but the circle
carried no members, so 'Famille' authorised no one. The MVC
admin controller (Yavsc.Org.Controllers.CircleMembersController)
existed but had no REST counterpart, so PostIt — which only
talks to the Yavsc.Blogs API — had no way to manage membership
at all.

This commit closes that gap end-to-end:

Server (Yavsc.Blogs)
- GET    /api/circle/{id}/members           list members
- POST   /api/circle/{id}/members           add a user (body { userId })
- DELETE /api/circle/{id}/members/{userId}  remove a user
  All three are scoped to caller == circle.OwnerId; non-owned
  circles return 404 (not 403) to avoid leaking existence, in
  line with the rest of the controller.
- Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the
  wire shapes. CircleMemberDto mirrors UserSearchResultDto
  minus Email — membership UI doesn't need contact details.

Tests (Yavsc.Blogs.Tests)
- CircleMembersApiTests: 5 [Fact] covering empty list,
  add+get, duplicate add → 409, remove, and cross-owner 404.
  Test users (alice, bob) are seeded directly through the
  in-memory DbContext — the Blogs fixture doesn't stand up
  UserManager<ApplicationUser>.

Client (Yavsc.Api.Client)
- CircleMemberDto + 3 methods on CircleApiClient:
  GetMembersAsync, AddMemberAsync, RemoveMemberAsync.
  All match the server's contract: 404 flattens to null,
  409 surfaces as an exception (callers can dedupe beforehand
  if they want idempotent behaviour).

UI (PostIt)
- CirclesPageViewModel gains a Members ObservableCollection
  that auto-loads on SelectedCircle change (via the partial
  setter generated by [ObservableProperty]). Commands:
  LoadMembersAsync, OpenAddMember (raises an event the view
  subscribes to), OnAddMemberConfirmedAsync (called by the
  view when the dialog confirms a selection), RemoveMemberAsync.
  409 (already a member) is detected from the exception
  message and surfaced as a friendly status rather than an
  error — a likely race when the same user gets added twice
  through two UI paths.

- CirclesPage layout is now two-pane (circles + editor on the
  left, members of the selected circle on the right). The
  member pane has an 'Ajouter un membre' button that opens
  AddCircleMemberDialog. Code-behind wires the dialog's
  Confirmed event back into the VM via an async lambda
  wrapper (EventHandler<T> wants void, the VM method is
  async Task).

- AddCircleMemberDialog is a ContentPage (light modal, same
  pattern as PostAclDialog). Its ViewModel consumes
  IUserDirectory — the abstraction introduced by 04a31709
  to fix the 'user search should not be IContactService'
  confusion. The dialog raises Confirmed with the picked
  UserSummary; the host (CirclesPage) is responsible for
  calling CircleApiClient.AddMemberAsync.

Out of scope (tracked in MEMORY.md, 2026-08-18):
- i18n: all visible text still hard-coded French.
- XAML accessibility audit of pre-existing pages.
- Avalonia.Headless UI tests of the new navigation flow.

Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests
green (was 15; +5 for CircleMembersApiTests), 44/44
Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +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