yavsc/src/Yavsc.Api.Client/Dtos/CircleMemberDto.cs

21 lines
789 B
C#
Raw Normal View History

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 af5e71aa 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
namespace Yavsc.Api.Client.Dtos;
/// <summary>
/// Wire format for <c>GET /api/circle/{id}/members</c>.
///
/// <para>Mirrors the server-side
/// <c>Yavsc.Blogs.Controllers.CircleMemberDto</c>. Intentionally
/// stops short of the Email field that
/// <see cref="UserSearchResultDto"/> carries — the circle
/// membership UI only needs a name and an avatar to render the
/// list. If the future ACL UI wants contact details, it can
/// fall back to <see cref="IYavscApiClient"/>'s other
/// endpoints rather than widening this shape.</para>
/// </summary>
public sealed class CircleMemberDto
{
public string Id { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string? FullName { get; set; }
public string? Avatar { get; set; }
}