2026-08-17 23:36:20 +01:00
|
|
|
using System.Linq;
|
2023-03-19 17:57:55 +00:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2019-01-01 16:28:47 +00:00
|
|
|
using Yavsc.Models;
|
|
|
|
|
using Yavsc.Models.Relationship;
|
2025-07-15 19:43:41 +01:00
|
|
|
using Yavsc.Server.Helpers;
|
2026-08-19 21:02:28 +01:00
|
|
|
using static Yavsc.Blogs.Constants;
|
2019-01-01 16:28:47 +00:00
|
|
|
|
2026-08-17 23:34:48 +01:00
|
|
|
namespace Yavsc.Blogs.Controllers
|
2019-01-01 16:28:47 +00:00
|
|
|
{
|
|
|
|
|
[Produces("application/json")]
|
2026-08-19 21:02:28 +01:00
|
|
|
[Route(APIPrefix +"/circle")]
|
2019-01-01 16:28:47 +00:00
|
|
|
public class CircleApiController : Controller
|
|
|
|
|
{
|
2020-10-09 19:35:39 +01:00
|
|
|
private readonly ApplicationDbContext _context;
|
2019-01-01 16:28:47 +00:00
|
|
|
|
|
|
|
|
public CircleApiController(ApplicationDbContext context)
|
|
|
|
|
{
|
|
|
|
|
_context = context;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:36:20 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the caller's own circles. Circles are personal —
|
|
|
|
|
/// the API never exposes another user's circles, even by id.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// GET: api/circle
|
2019-01-01 16:28:47 +00:00
|
|
|
[HttpGet]
|
|
|
|
|
public IEnumerable<Circle> GetCircle()
|
|
|
|
|
{
|
2026-08-18 14:02:07 +01:00
|
|
|
var uid = User.GetUserId();
|
2026-08-17 23:36:20 +01:00
|
|
|
return _context.Circle.Where(c => c.OwnerId == uid);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:36:20 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Returns a single circle only when it belongs to the caller.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// GET: api/circle/5
|
2019-01-01 16:28:47 +00:00
|
|
|
[HttpGet("{id}", Name = "GetCircle")]
|
|
|
|
|
public async Task<IActionResult> GetCircle([FromRoute] long id)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest(ModelState);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 14:02:07 +01:00
|
|
|
var uid = User.GetUserId();
|
2026-08-17 23:36:20 +01:00
|
|
|
Circle circle = await _context.Circle.SingleOrDefaultAsync(
|
|
|
|
|
m => m.Id == id && m.OwnerId == uid);
|
2019-01-01 16:28:47 +00:00
|
|
|
|
|
|
|
|
if (circle == null)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return NotFound();
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Ok(circle);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:36:20 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Replaces a circle. The caller must own it; the server
|
|
|
|
|
/// reasserts ownership regardless of any OwnerId the client
|
|
|
|
|
/// tries to put in the body.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// PUT: api/circle/5
|
2019-01-01 16:28:47 +00:00
|
|
|
[HttpPut("{id}")]
|
|
|
|
|
public async Task<IActionResult> PutCircle([FromRoute] long id, [FromBody] Circle circle)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest(ModelState);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (id != circle.Id)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest();
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 14:02:07 +01:00
|
|
|
var uid = User.GetUserId();
|
2026-08-17 23:36:20 +01:00
|
|
|
var existing = await _context.Circle.SingleOrDefaultAsync(
|
|
|
|
|
c => c.Id == id && c.OwnerId == uid);
|
|
|
|
|
if (existing is null)
|
|
|
|
|
{
|
|
|
|
|
return new ChallengeResult();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force OwnerId to the caller; the body value is ignored.
|
|
|
|
|
circle.OwnerId = uid;
|
2019-01-01 16:28:47 +00:00
|
|
|
_context.Entry(circle).State = EntityState.Modified;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _context.SaveChangesAsync(User.GetUserId());
|
|
|
|
|
}
|
|
|
|
|
catch (DbUpdateConcurrencyException)
|
|
|
|
|
{
|
|
|
|
|
if (!CircleExists(id))
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return NotFound();
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-19 17:57:55 +00:00
|
|
|
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:36:20 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a circle owned by the caller. The server overwrites
|
|
|
|
|
/// any OwnerId the client sends in the body.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// POST: api/circle
|
2019-01-01 16:28:47 +00:00
|
|
|
[HttpPost]
|
|
|
|
|
public async Task<IActionResult> PostCircle([FromBody] Circle circle)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest(ModelState);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 14:02:07 +01:00
|
|
|
var uid = User.GetUserId();
|
2026-08-17 23:36:20 +01:00
|
|
|
circle.OwnerId = uid;
|
|
|
|
|
|
2019-01-01 16:28:47 +00:00
|
|
|
_context.Circle.Add(circle);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _context.SaveChangesAsync(User.GetUserId());
|
|
|
|
|
}
|
|
|
|
|
catch (DbUpdateException)
|
|
|
|
|
{
|
|
|
|
|
if (CircleExists(circle.Id))
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return new StatusCodeResult(StatusCodes.Status409Conflict);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CreatedAtRoute("GetCircle", new { id = circle.Id }, circle);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:36:20 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Deletes a circle only if the caller owns it. Returns 404
|
|
|
|
|
/// (not 403) when the circle does not exist or is not owned
|
|
|
|
|
/// by the caller, to avoid leaking the existence of someone
|
|
|
|
|
/// else's circle.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// DELETE: api/circle/5
|
2019-01-01 16:28:47 +00:00
|
|
|
[HttpDelete("{id}")]
|
|
|
|
|
public async Task<IActionResult> DeleteCircle([FromRoute] long id)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest(ModelState);
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 14:02:07 +01:00
|
|
|
var uid = User.GetUserId();
|
2026-08-17 23:36:20 +01:00
|
|
|
Circle circle = await _context.Circle.SingleOrDefaultAsync(
|
|
|
|
|
m => m.Id == id && m.OwnerId == uid);
|
2019-01-01 16:28:47 +00:00
|
|
|
if (circle == null)
|
|
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
return NotFound();
|
2019-01-01 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_context.Circle.Remove(circle);
|
|
|
|
|
await _context.SaveChangesAsync(User.GetUserId());
|
|
|
|
|
|
|
|
|
|
return Ok(circle);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the members of one of the caller's circles.
|
|
|
|
|
/// Returns 404 (not 403) when the circle does not exist
|
|
|
|
|
/// or is not owned by the caller, mirroring the scoping
|
|
|
|
|
/// of the rest of this controller.
|
|
|
|
|
/// </summary>
|
|
|
|
|
// GET: api/circle/5/members
|
|
|
|
|
[HttpGet("{id}/members")]
|
|
|
|
|
public async Task<IActionResult> GetMembers([FromRoute] long id)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest(ModelState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var uid = User.GetUserId();
|
|
|
|
|
var ownsIt = await _context.Circle.AnyAsync(c => c.Id == id && c.OwnerId == uid);
|
|
|
|
|
if (!ownsIt)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var members = await _context.CircleMembers
|
|
|
|
|
.Where(m => m.CircleId == id)
|
|
|
|
|
.Select(m => new CircleMemberDto
|
|
|
|
|
{
|
|
|
|
|
Id = m.MemberId,
|
|
|
|
|
UserName = m.Member.UserName ?? string.Empty,
|
|
|
|
|
FullName = m.Member.FullName,
|
|
|
|
|
Avatar = m.Member.Avatar,
|
|
|
|
|
})
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
return Ok(members);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adds a Yavsc user to one of the caller's circles. The
|
|
|
|
|
/// body carries the user id (resolved client-side via the
|
|
|
|
|
/// central <c>/api/user-search</c> endpoint). Returns
|
|
|
|
|
/// 404 (not 403) when the circle does not exist or is not
|
|
|
|
|
/// owned by the caller, and 404 when the target user does
|
|
|
|
|
/// not exist, so the caller can't probe whether an email
|
|
|
|
|
/// belongs to a real account.
|
|
|
|
|
///
|
|
|
|
|
/// <para>Returns 409 Conflict if the user is already a
|
|
|
|
|
/// member of the circle; the client treats this as a
|
|
|
|
|
/// no-op success.</para>
|
|
|
|
|
/// </summary>
|
|
|
|
|
// POST: api/circle/5/members
|
|
|
|
|
// body: { "userId": "..." }
|
|
|
|
|
[HttpPost("{id}/members")]
|
|
|
|
|
public async Task<IActionResult> AddMember(
|
|
|
|
|
[FromRoute] long id,
|
|
|
|
|
[FromBody] AddCircleMemberDto body)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest(ModelState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var uid = User.GetUserId();
|
|
|
|
|
var ownsIt = await _context.Circle.AnyAsync(c => c.Id == id && c.OwnerId == uid);
|
|
|
|
|
if (!ownsIt)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reject unknown user ids the same way as an unknown
|
|
|
|
|
// circle: 404. Probing the user table by id should not
|
|
|
|
|
// be possible through this endpoint.
|
|
|
|
|
var userExists = await _context.Users.AnyAsync(u => u.Id == body.UserId);
|
|
|
|
|
if (!userExists)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Idempotency: re-adding an existing member is a
|
|
|
|
|
// 409, not a silent success. Clients that don't
|
|
|
|
|
// dedupe beforehand will at least get an actionable
|
|
|
|
|
// status code rather than a misleading "created".
|
|
|
|
|
var alreadyMember = await _context.CircleMembers.AnyAsync(
|
|
|
|
|
m => m.CircleId == id && m.MemberId == body.UserId);
|
|
|
|
|
if (alreadyMember)
|
|
|
|
|
{
|
|
|
|
|
return new StatusCodeResult(StatusCodes.Status409Conflict);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_context.CircleMembers.Add(new CircleMember
|
|
|
|
|
{
|
|
|
|
|
CircleId = id,
|
|
|
|
|
MemberId = body.UserId,
|
|
|
|
|
});
|
|
|
|
|
await _context.SaveChangesAsync(User.GetUserId());
|
|
|
|
|
|
|
|
|
|
return CreatedAtRoute("GetCircle", new { id }, body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Removes a user from one of the caller's circles.
|
|
|
|
|
/// Returns 404 when the circle does not exist or is not
|
|
|
|
|
/// owned by the caller, mirroring the rest of this
|
|
|
|
|
/// controller's scoping. Returns 404 when the user is
|
|
|
|
|
/// not a member of the circle (idempotent: removing a
|
|
|
|
|
/// non-member is the same as having nothing to remove).
|
|
|
|
|
/// </summary>
|
|
|
|
|
// DELETE: api/circle/5/members/tester
|
|
|
|
|
[HttpDelete("{id}/members/{userId}")]
|
|
|
|
|
public async Task<IActionResult> RemoveMember(
|
|
|
|
|
[FromRoute] long id,
|
|
|
|
|
[FromRoute] string userId)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest(ModelState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var uid = User.GetUserId();
|
|
|
|
|
var ownsIt = await _context.Circle.AnyAsync(c => c.Id == id && c.OwnerId == uid);
|
|
|
|
|
if (!ownsIt)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var membership = await _context.CircleMembers.SingleOrDefaultAsync(
|
|
|
|
|
m => m.CircleId == id && m.MemberId == userId);
|
|
|
|
|
if (membership is null)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_context.CircleMembers.Remove(membership);
|
|
|
|
|
await _context.SaveChangesAsync(User.GetUserId());
|
|
|
|
|
|
|
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-01 16:28:47 +00:00
|
|
|
protected override void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
_context.Dispose();
|
|
|
|
|
}
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool CircleExists(long id)
|
|
|
|
|
{
|
|
|
|
|
return _context.Circle.Count(e => e.Id == id) > 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wire shape for <c>GET /api/circle/{id}/members</c>.
|
|
|
|
|
/// Mirrors <see cref="UserSearchResultDto"/> but stops
|
|
|
|
|
/// short of the Email field — circle membership UI only
|
|
|
|
|
/// needs to render a name and an avatar, not contact
|
|
|
|
|
/// details.
|
|
|
|
|
/// </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; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wire shape for <c>POST /api/circle/{id}/members</c>.
|
|
|
|
|
/// The body is intentionally tiny: the client resolves
|
|
|
|
|
/// the user id via <c>/api/user-search</c> before
|
|
|
|
|
/// posting, so all we need is the resolved id.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class AddCircleMemberDto
|
|
|
|
|
{
|
|
|
|
|
public string UserId { get; set; } = string.Empty;
|
|
|
|
|
}
|
2020-10-09 19:35:39 +01:00
|
|
|
}
|