diff --git a/src/PostIt.Tests/BearerScopeTests.cs b/src/PostIt.Tests/BearerScopeTests.cs index 1f47a176..68fa514e 100644 --- a/src/PostIt.Tests/BearerScopeTests.cs +++ b/src/PostIt.Tests/BearerScopeTests.cs @@ -8,9 +8,6 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Yavsc.Blogspot; -using Yavsc.Api.Client; -using PostIt.Services; using PostIt.Services; using Xunit; @@ -122,7 +119,7 @@ public class BearerScopeTests // Resolve a BlogApiClient on top. We don't need real // posts; we just need the outbound HTTP request to be // the one we capture. - var blog = new BlogApiClient(subClient, "http://localhost/"); + var blog = new BlogApiClient(subClient); await blog.GetPostsAsync(ct: TestContext.Current.CancellationToken); diff --git a/src/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt.Tests/MainPageSaveTests.cs index 350a71f1..cea3e83f 100644 --- a/src/PostIt.Tests/MainPageSaveTests.cs +++ b/src/PostIt.Tests/MainPageSaveTests.cs @@ -3,7 +3,6 @@ using Avalonia.Controls; using Avalonia.Headless.XUnit; using Avalonia.VisualTree; using Yavsc.Blogspot; -using Yavsc.Api.Client; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; @@ -41,7 +40,7 @@ public class MainPageSaveTests // not a Control, so it needs a navigation host). var recorder = new CallRecorder(); var api = new RecordingYavscApiClient(recorder); - var blog = new BlogApiClient(api, "http://localhost/"); + var blog = new BlogApiClient(api); var viewModel = new MainPageViewModel(blog); var page = new MainPage { DataContext = viewModel }; diff --git a/src/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt.Tests/PostItViewModelTests.cs index b964a18e..d8de8025 100644 --- a/src/PostIt.Tests/PostItViewModelTests.cs +++ b/src/PostIt.Tests/PostItViewModelTests.cs @@ -1,5 +1,4 @@ using Yavsc.Blogspot; -using Yavsc.Api.Client; using PostIt.Services; using PostIt.ViewModels; @@ -15,7 +14,7 @@ public class PostItViewModelTests // default; tests construct one with a fake YavscApiClient that // throws on any call (we never call the API in this test). var fakeApi = new ThrowingYavscApiClient(); - var blog = new BlogApiClient(fakeApi, "http://localhost/"); + var blog = new BlogApiClient(fakeApi); var viewModel = new MainPageViewModel(blog); viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" }); @@ -47,7 +46,7 @@ public class PostItViewModelTests new() { Id = 2, Title = "World" } }; var api = new StubYavscApiClient(expected); - var blog = new BlogApiClient(api, "http://localhost/"); + var blog = new BlogApiClient(api); var posts = await blog.GetPostsAsync(); diff --git a/src/PostIt.Tests/YavscApiClientTests.cs b/src/PostIt.Tests/YavscApiClientTests.cs index e54bc541..c020fec9 100644 --- a/src/PostIt.Tests/YavscApiClientTests.cs +++ b/src/PostIt.Tests/YavscApiClientTests.cs @@ -8,9 +8,6 @@ using System.Net.Sockets; using System.Text; using System.Text.Json; using System.Threading; -using Yavsc.Blogspot; -using Yavsc.Api.Client; -using PostIt.Services; using System.Threading.Tasks; using IdentityModel.OidcClient; using IdentityModel.OidcClient.Browser; diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 4a250ebe..b5740f2f 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -7,7 +7,6 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Styling; using PostIt.Services; -using Yavsc.Api.Client; using PostIt.ViewModels; using PostIt.Views; @@ -56,7 +55,7 @@ public partial class App : Application "PostIt", "tokens.json")); var api = new YavscApiClient(settings, tokenStore); - var client = new BlogApiClient(api, settings.BlogsApiUrl); + var client = new BlogApiClient(api); var services = new ServiceCollection(); diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index e4d51a88..d9cf96d3 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -25,7 +25,6 @@ - diff --git a/src/Yavsc.Api.Client/BlogApiClient.cs b/src/PostIt/PostIt/Services/BlogApiClient.cs similarity index 78% rename from src/Yavsc.Api.Client/BlogApiClient.cs rename to src/PostIt/PostIt/Services/BlogApiClient.cs index 537124a7..f2061927 100644 --- a/src/Yavsc.Api.Client/BlogApiClient.cs +++ b/src/PostIt/PostIt/Services/BlogApiClient.cs @@ -5,16 +5,15 @@ using System.Threading; using System.Threading.Tasks; using Yavsc.Blogspot; -namespace Yavsc.Api.Client; +namespace PostIt.Services; /// /// High-level client for the Blog subsystem of the Yavsc API /// (deployed at https://blogs.pschneider.fr). All transport /// concerns — base URL, JSON serialisation, Bearer auth, silent /// refresh on 401, request body shaping — are delegated to -/// , which lives in the consuming -/// application (PostIt). This class is a thin DTO↔path mapper, -/// nothing more. +/// . This class is a thin DTO↔path +/// mapper, nothing more. /// /// URL convention. 's /// BaseAddress already terminates with /api/v1/ @@ -35,20 +34,16 @@ public sealed class BlogApiClient { private const string DefaultPathPrefix = "blog"; - private readonly IYavscApiClient _api; - private readonly Uri _baseAddress; + private readonly YavscApiClient _api; private readonly string _pathPrefix; - public BlogApiClient(IYavscApiClient api, string blogsBaseAddress, string pathPrefix = DefaultPathPrefix) + public BlogApiClient(YavscApiClient api, string pathPrefix = DefaultPathPrefix) { _api = api ?? throw new ArgumentNullException(nameof(api)); - if (string.IsNullOrEmpty(blogsBaseAddress)) - throw new ArgumentException("Base address is required.", nameof(blogsBaseAddress)); - // e.g. "https://blogs.pschneider.fr/api/v1/" — keep the + // ApiUrl is e.g. "https://blogs.pschneider.fr/api/v1/" — keep the // trailing slash so relative paths ("posts") resolve correctly. - _baseAddress = new Uri(blogsBaseAddress); - api.Http.BaseAddress = _baseAddress; + api.Http.BaseAddress = new Uri(api.Settings.BlogsApiUrl); _pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix; } diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index b611fe02..9ae1453b 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using IdentityModel.OidcClient; using PostIt.ViewModels; -using Yavsc.Api.Client; namespace PostIt.Services; @@ -25,7 +24,7 @@ namespace PostIt.Services; /// only refreshes once even if many /// concurrent requests are in flight. /// -public class YavscApiClient : IYavscApiClient, IAsyncDisposable +public class YavscApiClient : IAsyncDisposable { // 60s of slack before the access_token's nominal expiry. Covers // network latency + JWT validation on the server side. diff --git a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs index a9864db0..e8024d7d 100644 --- a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Yavsc.Blogspot; -using Yavsc.Api.Client; using PostIt.Services; namespace PostIt.ViewModels; diff --git a/src/Yavsc.Api.Client/BlogAclApiClient.cs b/src/Yavsc.Api.Client/BlogAclApiClient.cs deleted file mode 100644 index 71263ca4..00000000 --- a/src/Yavsc.Api.Client/BlogAclApiClient.cs +++ /dev/null @@ -1,49 +0,0 @@ -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; - -/// -/// HTTP client for /api/blogacl on the Yavsc Blogs server. -/// -/// Each grants a single -/// Circle access to a single BlogPost. 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. -/// -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> GetMyAclAsync(CancellationToken ct = default) - => _api.CallAsync>(HttpMethod.Get, Path, ct: ct); - - public Task GetAclAsync(long circleId, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Get, $"{Path}/{circleId}", ct: ct); - - public Task GrantAsync(CircleAuthorizationDto acl, CancellationToken ct = default) - => _api.CallAsync(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); -} diff --git a/src/Yavsc.Api.Client/CircleApiClient.cs b/src/Yavsc.Api.Client/CircleApiClient.cs deleted file mode 100644 index a8b04a40..00000000 --- a/src/Yavsc.Api.Client/CircleApiClient.cs +++ /dev/null @@ -1,53 +0,0 @@ -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; - -/// -/// HTTP client for /api/circle on the Yavsc Blogs server. -/// -/// Same conventions as : all -/// transport is delegated to ; this -/// class only maps paths to DTOs. -/// -/// 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. -/// -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> GetMyCirclesAsync(CancellationToken ct = default) - => _api.CallAsync>(HttpMethod.Get, Path, ct: ct); - - public Task GetCircleAsync(long id, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Get, $"{Path}/{id}", ct: ct); - - public Task CreateCircleAsync(CircleDto circle, CancellationToken ct = default) - => _api.CallAsync(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); -} diff --git a/src/Yavsc.Api.Client/Dtos/CircleAuthorizationDto.cs b/src/Yavsc.Api.Client/Dtos/CircleAuthorizationDto.cs deleted file mode 100644 index f5d1e50e..00000000 --- a/src/Yavsc.Api.Client/Dtos/CircleAuthorizationDto.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Yavsc.Api.Client.Dtos; - -/// -/// Wire format for GET /api/blogacl and friends. -/// -/// The server-side -/// Yavsc.Models.Access.CircleAuthorizationToBlogPost EF entity -/// carries virtual navigation properties (Target, -/// Allowed) 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 GET /api/circle. -/// -public sealed class CircleAuthorizationDto -{ - public long CircleId { get; set; } - public long BlogPostId { get; set; } - public bool Comment { get; set; } -} diff --git a/src/Yavsc.Api.Client/Dtos/CircleDto.cs b/src/Yavsc.Api.Client/Dtos/CircleDto.cs deleted file mode 100644 index ed6980e2..00000000 --- a/src/Yavsc.Api.Client/Dtos/CircleDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Yavsc.Api.Client.Dtos; - -/// -/// Wire format for GET /api/circle and friends. -/// -/// Field names match the JSON the server emits (camelCase via -/// the default policy), so no -/// [JsonPropertyName] attributes are required. -/// -/// Mirrors the server-side Yavsc.Models.Relationship.Circle -/// EF entity but stops short of the navigation properties -/// (Owner, Members) which depend on -/// ApplicationUser and other server-only types. The client -/// only ever needs the id, name, and owner of a circle to drive -/// the UI. -/// -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; } -} diff --git a/src/Yavsc.Api.Client/IYavscApiClient.cs b/src/Yavsc.Api.Client/IYavscApiClient.cs deleted file mode 100644 index 209ec07d..00000000 --- a/src/Yavsc.Api.Client/IYavscApiClient.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Yavsc.Api.Client; - -/// -/// Transport surface that the high-level clients -/// (, , -/// ) need to do their work. -/// -/// This is intentionally a thin, transport-only contract. It -/// does not include the OIDC login / refresh / logout surface — -/// that lives on the concrete YavscApiClient in the -/// consuming application and is wired by the application -/// composition root. Splitting the two keeps Yavsc.Api.Client -/// usable from any host (a CLI, a unit test, a future iOS -/// client) without dragging OIDC, identity, and a Settings -/// POMVO everywhere. -/// -/// Implementations are expected to: -/// -/// Attach a Bearer access token to every outbound request. -/// Silently refresh the token on a 401 and retry once. -/// Serialise the request body as JSON and deserialise the -/// response body with case-insensitive property matching. -/// -/// -/// The exception contract on non-2xx responses is -/// with a message that includes -/// the response body (capped), so callers can surface the -/// server-side validation problem to the UI without losing -/// context. -/// -public interface IYavscApiClient : IAsyncDisposable -{ - /// - /// The configured . Clients set its - /// BaseAddress in their constructors to point at the - /// API host they target. - /// - HttpClient Http { get; } - - /// Call a JSON endpoint with a typed return value. - /// HTTP verb. - /// Path relative to . - /// Optional request body, serialised as JSON. - /// Cancellation token. - Task CallAsync( - HttpMethod method, - string path, - object? body = null, - CancellationToken ct = default); - - /// Call a JSON endpoint that returns no useful body (DELETE, 204, etc.). - Task CallAsync( - HttpMethod method, - string path, - object? body = null, - CancellationToken ct = default); -} diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj deleted file mode 100644 index 5376856d..00000000 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - net10.0 - enable - Yavsc.Api.Client - Yavsc.Api.Client - enable - latest - true - - 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). - - https://github.com/pazof/yavsc - true - 1.0.1.0 - 1.0.1.0 - 1.0.1-5+Branch.main.Sha.0617fc6bda7151c70559d87177e2dcfb1b60995f - 1.0.1-5 - - - - - - - -