release/1.0.7 #34
4 changed files with 105 additions and 4 deletions
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.
commit
6e7e04141b
|
|
@ -59,6 +59,7 @@ public partial class App : Application
|
|||
var client = new BlogApiClient(api, settings.BlogsApiUrl);
|
||||
var circleClient = new CircleApiClient(api, settings.BlogsApiUrl);
|
||||
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
|
||||
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ public partial class App : Application
|
|||
services.AddSingleton(client);
|
||||
services.AddSingleton(circleClient);
|
||||
services.AddSingleton(blogAclClient);
|
||||
services.AddSingleton(userSearchClient);
|
||||
services.AddTransient<MainPageViewModel>();
|
||||
services.AddTransient<HomePageViewModel>();
|
||||
services.AddTransient<SignaturePageViewModel>();
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<UseMaui>true</UseMaui>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AssemblyVersion>1.0.1.0</AssemblyVersion>
|
||||
<FileVersion>1.0.1.0</FileVersion>
|
||||
<InformationalVersion>1.0.1-5+Branch.main.Sha.0617fc6bda7151c70559d87177e2dcfb1b60995f</InformationalVersion>
|
||||
<Version>1.0.1-5</Version>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
|
|
@ -26,7 +24,6 @@
|
|||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Maui.Essentials" />
|
||||
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||
<ProjectReference Include="../../Yavsc.Api.Client/Yavsc.Api.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
|
@ -43,4 +40,4 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
23
src/Yavsc.Api.Client/Dtos/UserSearchResultDto.cs
Normal file
23
src/Yavsc.Api.Client/Dtos/UserSearchResultDto.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
namespace Yavsc.Api.Client.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Wire format for <c>GET /api/user-search</c>.
|
||||
///
|
||||
/// <para>Mirrors the server-side
|
||||
/// <c>Yavsc.Blogs.Controllers.UserSearchResultDto</c> but stops
|
||||
/// short of any entity navigation properties. Only the fields
|
||||
/// a client address book needs (id, name, avatar, email) are
|
||||
/// included.</para>
|
||||
///
|
||||
/// <para>Field names match the JSON the server emits (camelCase
|
||||
/// via the default <see cref="System.Text.Json"/> policy), so
|
||||
/// no <c>[JsonPropertyName]</c> attributes are required.</para>
|
||||
/// </summary>
|
||||
public sealed class UserSearchResultDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string? FullName { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public string? Email { get; set; }
|
||||
}
|
||||
79
src/Yavsc.Api.Client/UserSearchClient.cs
Normal file
79
src/Yavsc.Api.Client/UserSearchClient.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Yavsc.Api.Client.Dtos;
|
||||
|
||||
namespace Yavsc.Api.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for <c>/api/user-search</c> on the Yavsc Blogs
|
||||
/// server. Used by client-side address books (PostIt.Desktop,
|
||||
/// future PostIt.Browser CLI, …) to look up Yavsc users by
|
||||
/// display name or email.
|
||||
///
|
||||
/// <para>The server scopes every endpoint to the authenticated
|
||||
/// caller; any authenticated user can search the user table of
|
||||
/// the instance. There is no per-user filtering on the response
|
||||
/// side — this is by design on single-tenant deployments
|
||||
/// (closed community). Multi-tenant deployments should gate
|
||||
/// this controller behind a tenant-scoped policy before
|
||||
/// exposing it; see the server-side
|
||||
/// <c>UserSearchApiController</c> doc for details.</para>
|
||||
/// </summary>
|
||||
public sealed class UserSearchClient
|
||||
{
|
||||
private const string Path = "user-search";
|
||||
|
||||
private readonly IYavscApiClient _api;
|
||||
|
||||
public UserSearchClient(IYavscApiClient api, string blogsBaseAddress)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
if (string.IsNullOrEmpty(blogsBaseAddress))
|
||||
throw new ArgumentException("Base address is required.", nameof(blogsBaseAddress));
|
||||
|
||||
if (api.Http.BaseAddress is null)
|
||||
api.Http.BaseAddress = new Uri(blogsBaseAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search users by display name (substring) or email (exact).
|
||||
/// </summary>
|
||||
/// <param name="query">Substring filter on FullName or
|
||||
/// UserName. Empty or null returns an empty list (the server
|
||||
/// would return all users, which we don't want by
|
||||
/// default).</param>
|
||||
/// <param name="email">Optional exact-match filter on
|
||||
/// Email.</param>
|
||||
/// <param name="take">Maximum results, capped at 100.
|
||||
/// Default 25.</param>
|
||||
public Task<List<UserSearchResultDto>> SearchAsync(
|
||||
string? query = null,
|
||||
string? email = null,
|
||||
int take = 25,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Match the server's contract: at least one filter is
|
||||
// expected. The server doesn't enforce this (an empty
|
||||
// query + empty email returns the first `take` users
|
||||
// alphabetically), but the address-book UX is "type
|
||||
// something to search", so we short-circuit empty
|
||||
// queries client-side.
|
||||
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(email))
|
||||
return Task.FromResult(new List<UserSearchResultDto>());
|
||||
|
||||
var qs = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
qs.Add($"q={Uri.EscapeDataString(query)}");
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
qs.Add($"e={Uri.EscapeDataString(email)}");
|
||||
qs.Add($"take={Math.Clamp(take, 1, 100)}");
|
||||
|
||||
return _api.CallAsync<List<UserSearchResultDto>>(
|
||||
HttpMethod.Get,
|
||||
$"{Path}?{string.Join('&', qs)}",
|
||||
ct: ct);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue