Compare commits
2 commits
0e95e28327
...
f835ad42a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
f835ad42a1 |
|||
|
ab40af8ef1 |
15 changed files with 265 additions and 13 deletions
|
|
@ -8,6 +8,9 @@ using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
|
using PostIt.Services;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -119,7 +122,7 @@ public class BearerScopeTests
|
||||||
// Resolve a BlogApiClient on top. We don't need real
|
// Resolve a BlogApiClient on top. We don't need real
|
||||||
// posts; we just need the outbound HTTP request to be
|
// posts; we just need the outbound HTTP request to be
|
||||||
// the one we capture.
|
// the one we capture.
|
||||||
var blog = new BlogApiClient(subClient);
|
var blog = new BlogApiClient(subClient, "http://localhost/");
|
||||||
|
|
||||||
await blog.GetPostsAsync(ct: TestContext.Current.CancellationToken);
|
await blog.GetPostsAsync(ct: TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Headless.XUnit;
|
using Avalonia.Headless.XUnit;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using Yavsc.Blogspot;
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
using PostIt.Views;
|
using PostIt.Views;
|
||||||
|
|
@ -40,7 +41,7 @@ public class MainPageSaveTests
|
||||||
// not a Control, so it needs a navigation host).
|
// not a Control, so it needs a navigation host).
|
||||||
var recorder = new CallRecorder();
|
var recorder = new CallRecorder();
|
||||||
var api = new RecordingYavscApiClient(recorder);
|
var api = new RecordingYavscApiClient(recorder);
|
||||||
var blog = new BlogApiClient(api);
|
var blog = new BlogApiClient(api, "http://localhost/");
|
||||||
var viewModel = new MainPageViewModel(blog);
|
var viewModel = new MainPageViewModel(blog);
|
||||||
|
|
||||||
var page = new MainPage { DataContext = viewModel };
|
var page = new MainPage { DataContext = viewModel };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Yavsc.Blogspot;
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ public class PostItViewModelTests
|
||||||
// default; tests construct one with a fake YavscApiClient that
|
// default; tests construct one with a fake YavscApiClient that
|
||||||
// throws on any call (we never call the API in this test).
|
// throws on any call (we never call the API in this test).
|
||||||
var fakeApi = new ThrowingYavscApiClient();
|
var fakeApi = new ThrowingYavscApiClient();
|
||||||
var blog = new BlogApiClient(fakeApi);
|
var blog = new BlogApiClient(fakeApi, "http://localhost/");
|
||||||
var viewModel = new MainPageViewModel(blog);
|
var viewModel = new MainPageViewModel(blog);
|
||||||
|
|
||||||
viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
|
viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
|
||||||
|
|
@ -46,7 +47,7 @@ public class PostItViewModelTests
|
||||||
new() { Id = 2, Title = "World" }
|
new() { Id = 2, Title = "World" }
|
||||||
};
|
};
|
||||||
var api = new StubYavscApiClient(expected);
|
var api = new StubYavscApiClient(expected);
|
||||||
var blog = new BlogApiClient(api);
|
var blog = new BlogApiClient(api, "http://localhost/");
|
||||||
|
|
||||||
var posts = await blog.GetPostsAsync();
|
var posts = await blog.GetPostsAsync();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
|
using PostIt.Services;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel.OidcClient;
|
using IdentityModel.OidcClient;
|
||||||
using IdentityModel.OidcClient.Browser;
|
using IdentityModel.OidcClient.Browser;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
using PostIt.Views;
|
using PostIt.Views;
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ public partial class App : Application
|
||||||
"PostIt", "tokens.json"));
|
"PostIt", "tokens.json"));
|
||||||
|
|
||||||
var api = new YavscApiClient(settings, tokenStore);
|
var api = new YavscApiClient(settings, tokenStore);
|
||||||
var client = new BlogApiClient(api);
|
var client = new BlogApiClient(api, settings.BlogsApiUrl);
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
<PackageReference Include="IdentityModel.OidcClient" />
|
<PackageReference Include="IdentityModel.OidcClient" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||||
|
<ProjectReference Include="../../Yavsc.Api.Client/Yavsc.Api.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="postit-settings.json">
|
<Content Include="postit-settings.json">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel.OidcClient;
|
using IdentityModel.OidcClient;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
|
|
||||||
namespace PostIt.Services;
|
namespace PostIt.Services;
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ namespace PostIt.Services;
|
||||||
/// <see cref="BearerTokenHandler"/> only refreshes once even if many
|
/// <see cref="BearerTokenHandler"/> only refreshes once even if many
|
||||||
/// concurrent requests are in flight.
|
/// concurrent requests are in flight.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class YavscApiClient : IAsyncDisposable
|
public class YavscApiClient : IYavscApiClient, IAsyncDisposable
|
||||||
{
|
{
|
||||||
// 60s of slack before the access_token's nominal expiry. Covers
|
// 60s of slack before the access_token's nominal expiry. Covers
|
||||||
// network latency + JWT validation on the server side.
|
// network latency + JWT validation on the server side.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Yavsc.Blogspot;
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
namespace PostIt.ViewModels;
|
||||||
|
|
|
||||||
49
src/Yavsc.Api.Client/BlogAclApiClient.cs
Normal file
49
src/Yavsc.Api.Client/BlogAclApiClient.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
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/blogacl</c> on the Yavsc Blogs server.
|
||||||
|
///
|
||||||
|
/// <para>Each <see cref="CircleAuthorizationDto"/> grants a single
|
||||||
|
/// <c>Circle</c> access to a single <c>BlogPost</c>. The server
|
||||||
|
/// scopes every endpoint to the caller's uid: only the author of
|
||||||
|
/// the underlying blog post can list, create, modify, or delete
|
||||||
|
/// its ACL entries.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlogAclApiClient
|
||||||
|
{
|
||||||
|
private const string Path = "blogacl";
|
||||||
|
|
||||||
|
private readonly IYavscApiClient _api;
|
||||||
|
|
||||||
|
public BlogAclApiClient(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<CircleAuthorizationDto>> GetMyAclAsync(CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<List<CircleAuthorizationDto>>(HttpMethod.Get, Path, ct: ct);
|
||||||
|
|
||||||
|
public Task<CircleAuthorizationDto?> GetAclAsync(long circleId, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<CircleAuthorizationDto?>(HttpMethod.Get, $"{Path}/{circleId}", ct: ct);
|
||||||
|
|
||||||
|
public Task<CircleAuthorizationDto?> GrantAsync(CircleAuthorizationDto acl, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<CircleAuthorizationDto?>(HttpMethod.Post, Path, body: acl, ct: ct);
|
||||||
|
|
||||||
|
public Task UpdateAclAsync(long circleId, CircleAuthorizationDto acl, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync(HttpMethod.Put, $"{Path}/{circleId}", body: acl, ct: ct);
|
||||||
|
|
||||||
|
public Task RevokeAsync(long circleId, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync(HttpMethod.Delete, $"{Path}/{circleId}", ct: ct);
|
||||||
|
}
|
||||||
|
|
@ -5,15 +5,16 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Yavsc.Blogspot;
|
using Yavsc.Blogspot;
|
||||||
|
|
||||||
namespace PostIt.Services;
|
namespace Yavsc.Api.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// High-level client for the Blog subsystem of the Yavsc API
|
/// High-level client for the Blog subsystem of the Yavsc API
|
||||||
/// (deployed at <c>https://blogs.pschneider.fr</c>). All transport
|
/// (deployed at <c>https://blogs.pschneider.fr</c>). All transport
|
||||||
/// concerns — base URL, JSON serialisation, Bearer auth, silent
|
/// concerns — base URL, JSON serialisation, Bearer auth, silent
|
||||||
/// refresh on 401, request body shaping — are delegated to
|
/// refresh on 401, request body shaping — are delegated to
|
||||||
/// <see cref="YavscApiClient"/>. This class is a thin DTO↔path
|
/// <see cref="YavscApiClient"/>, which lives in the consuming
|
||||||
/// mapper, nothing more.
|
/// application (PostIt). This class is a thin DTO↔path mapper,
|
||||||
|
/// nothing more.
|
||||||
///
|
///
|
||||||
/// <para><b>URL convention.</b> <see cref="YavscApiClient"/>'s
|
/// <para><b>URL convention.</b> <see cref="YavscApiClient"/>'s
|
||||||
/// <c>BaseAddress</c> already terminates with <c>/api/v1/</c>
|
/// <c>BaseAddress</c> already terminates with <c>/api/v1/</c>
|
||||||
|
|
@ -34,16 +35,20 @@ public sealed class BlogApiClient
|
||||||
{
|
{
|
||||||
private const string DefaultPathPrefix = "blog";
|
private const string DefaultPathPrefix = "blog";
|
||||||
|
|
||||||
private readonly YavscApiClient _api;
|
private readonly IYavscApiClient _api;
|
||||||
|
private readonly Uri _baseAddress;
|
||||||
private readonly string _pathPrefix;
|
private readonly string _pathPrefix;
|
||||||
|
|
||||||
public BlogApiClient(YavscApiClient api, string pathPrefix = DefaultPathPrefix)
|
public BlogApiClient(IYavscApiClient api, string blogsBaseAddress, string pathPrefix = DefaultPathPrefix)
|
||||||
{
|
{
|
||||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||||
|
if (string.IsNullOrEmpty(blogsBaseAddress))
|
||||||
|
throw new ArgumentException("Base address is required.", nameof(blogsBaseAddress));
|
||||||
|
|
||||||
// ApiUrl is e.g. "https://blogs.pschneider.fr/api/v1/" — keep the
|
// e.g. "https://blogs.pschneider.fr/api/v1/" — keep the
|
||||||
// trailing slash so relative paths ("posts") resolve correctly.
|
// trailing slash so relative paths ("posts") resolve correctly.
|
||||||
api.Http.BaseAddress = new Uri(api.Settings.BlogsApiUrl);
|
_baseAddress = new Uri(blogsBaseAddress);
|
||||||
|
api.Http.BaseAddress = _baseAddress;
|
||||||
|
|
||||||
_pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix;
|
_pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix;
|
||||||
}
|
}
|
||||||
53
src/Yavsc.Api.Client/CircleApiClient.cs
Normal file
53
src/Yavsc.Api.Client/CircleApiClient.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
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/circle</c> on the Yavsc Blogs server.
|
||||||
|
///
|
||||||
|
/// <para>Same conventions as <see cref="BlogApiClient"/>: all
|
||||||
|
/// transport is delegated to <see cref="YavscApiClient"/>; this
|
||||||
|
/// class only maps paths to DTOs.</para>
|
||||||
|
///
|
||||||
|
/// <para>The server now (since the BlogAcl fix on this branch)
|
||||||
|
/// scopes every read and write to the caller's uid. There is no
|
||||||
|
/// way for the client to read or modify another user's circles
|
||||||
|
/// — the route will return 404 (not 403) when the circle exists
|
||||||
|
/// but belongs to someone else, to avoid leaking its existence.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CircleApiClient
|
||||||
|
{
|
||||||
|
private const string Path = "circle";
|
||||||
|
|
||||||
|
private readonly IYavscApiClient _api;
|
||||||
|
|
||||||
|
public CircleApiClient(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<CircleDto>> GetMyCirclesAsync(CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<List<CircleDto>>(HttpMethod.Get, Path, ct: ct);
|
||||||
|
|
||||||
|
public Task<CircleDto?> GetCircleAsync(long id, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<CircleDto?>(HttpMethod.Get, $"{Path}/{id}", ct: ct);
|
||||||
|
|
||||||
|
public Task<CircleDto?> CreateCircleAsync(CircleDto circle, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync<CircleDto?>(HttpMethod.Post, Path, body: circle, ct: ct);
|
||||||
|
|
||||||
|
public Task UpdateCircleAsync(long id, CircleDto circle, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync(HttpMethod.Put, $"{Path}/{id}", body: circle, ct: ct);
|
||||||
|
|
||||||
|
public Task DeleteCircleAsync(long id, CancellationToken ct = default)
|
||||||
|
=> _api.CallAsync(HttpMethod.Delete, $"{Path}/{id}", ct: ct);
|
||||||
|
}
|
||||||
19
src/Yavsc.Api.Client/Dtos/CircleAuthorizationDto.cs
Normal file
19
src/Yavsc.Api.Client/Dtos/CircleAuthorizationDto.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
namespace Yavsc.Api.Client.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire format for <c>GET /api/blogacl</c> and friends.
|
||||||
|
///
|
||||||
|
/// <para>The server-side
|
||||||
|
/// <c>Yavsc.Models.Access.CircleAuthorizationToBlogPost</c> EF entity
|
||||||
|
/// carries virtual navigation properties (<c>Target</c>,
|
||||||
|
/// <c>Allowed</c>) that pull in the full BlogPost and Circle graphs.
|
||||||
|
/// The client never needs them: when showing the ACL of a post, the
|
||||||
|
/// UI already has the post, and the circles are looked up by id
|
||||||
|
/// against the list returned by <c>GET /api/circle</c>.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CircleAuthorizationDto
|
||||||
|
{
|
||||||
|
public long CircleId { get; set; }
|
||||||
|
public long BlogPostId { get; set; }
|
||||||
|
public bool Comment { get; set; }
|
||||||
|
}
|
||||||
23
src/Yavsc.Api.Client/Dtos/CircleDto.cs
Normal file
23
src/Yavsc.Api.Client/Dtos/CircleDto.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Yavsc.Api.Client.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire format for <c>GET /api/circle</c> and friends.
|
||||||
|
///
|
||||||
|
/// <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>
|
||||||
|
///
|
||||||
|
/// <para>Mirrors the server-side <c>Yavsc.Models.Relationship.Circle</c>
|
||||||
|
/// EF entity but stops short of the navigation properties
|
||||||
|
/// (<c>Owner</c>, <c>Members</c>) which depend on
|
||||||
|
/// <c>ApplicationUser</c> and other server-only types. The client
|
||||||
|
/// only ever needs the id, name, and owner of a circle to drive
|
||||||
|
/// the UI.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CircleDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string OwnerId { get; set; } = string.Empty;
|
||||||
|
public bool Public { get; set; }
|
||||||
|
}
|
||||||
62
src/Yavsc.Api.Client/IYavscApiClient.cs
Normal file
62
src/Yavsc.Api.Client/IYavscApiClient.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Yavsc.Api.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transport surface that the high-level clients
|
||||||
|
/// (<see cref="BlogApiClient"/>, <see cref="CircleApiClient"/>,
|
||||||
|
/// <see cref="BlogAclApiClient"/>) need to do their work.
|
||||||
|
///
|
||||||
|
/// <para>This is intentionally a thin, transport-only contract. It
|
||||||
|
/// does not include the OIDC login / refresh / logout surface —
|
||||||
|
/// that lives on the concrete <c>YavscApiClient</c> in the
|
||||||
|
/// consuming application and is wired by the application
|
||||||
|
/// composition root. Splitting the two keeps <c>Yavsc.Api.Client</c>
|
||||||
|
/// usable from any host (a CLI, a unit test, a future iOS
|
||||||
|
/// client) without dragging OIDC, identity, and a <c>Settings</c>
|
||||||
|
/// POMVO everywhere.</para>
|
||||||
|
///
|
||||||
|
/// <para>Implementations are expected to:</para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Attach a Bearer access token to every outbound request.</item>
|
||||||
|
/// <item>Silently refresh the token on a 401 and retry once.</item>
|
||||||
|
/// <item>Serialise the request body as JSON and deserialise the
|
||||||
|
/// response body with case-insensitive property matching.</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// The exception contract on non-2xx responses is
|
||||||
|
/// <see cref="HttpRequestException"/> with a message that includes
|
||||||
|
/// the response body (capped), so callers can surface the
|
||||||
|
/// server-side validation problem to the UI without losing
|
||||||
|
/// context.
|
||||||
|
/// </summary>
|
||||||
|
public interface IYavscApiClient : IAsyncDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The configured <see cref="HttpClient"/>. Clients set its
|
||||||
|
/// <c>BaseAddress</c> in their constructors to point at the
|
||||||
|
/// API host they target.
|
||||||
|
/// </summary>
|
||||||
|
HttpClient Http { get; }
|
||||||
|
|
||||||
|
/// <summary>Call a JSON endpoint with a typed return value.</summary>
|
||||||
|
/// <param name="method">HTTP verb.</param>
|
||||||
|
/// <param name="path">Path relative to <see cref="HttpClient.BaseAddress"/>.</param>
|
||||||
|
/// <param name="body">Optional request body, serialised as JSON.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
Task<T> CallAsync<T>(
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
object? body = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Call a JSON endpoint that returns no useful body (DELETE, 204, etc.).</summary>
|
||||||
|
Task CallAsync(
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
object? body = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
29
src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
Normal file
29
src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>Yavsc.Api.Client</RootNamespace>
|
||||||
|
<AssemblyName>Yavsc.Api.Client</AssemblyName>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
<Description>
|
||||||
|
Thin HTTP clients for the Yavsc API. Each client is a DTO↔path
|
||||||
|
mapper; all transport concerns (base URL, JSON, Bearer auth,
|
||||||
|
silent refresh on 401) are delegated to YavscApiClient, which
|
||||||
|
lives in the consuming application (PostIt).
|
||||||
|
</Description>
|
||||||
|
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
|
||||||
|
<Library>true</Library>
|
||||||
|
<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>
|
||||||
|
<PackageReference Include="GitVersion.MsBuild" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue