Commit graph

856 commits

Author SHA1 Message Date
04a31709a2
refactor(postit): split IContactService from IUserDirectory
IContactService used to be the catch-all for "people you can reach
from PostIt": on mobile it read the device-local address book, on
desktop it queried the central /api/user-search endpoint and merged
both worlds into a single ContactDto (a flat Email field, an
ObservableCollection cache, a SearchAsync method). Two unrelated
flows under the same name, with a wire shape (Email) silently
flattening the mobile provider's multi-email list.

Split into two interfaces, each with a single responsibility:

- IContactService: device-local address book only. Mobile provider
  reads MAUI Essentials Contacts.Default and carries the full email
  list per contact. Desktop provider is an honest stub returning an
  empty list — the desktop has no local address book, and inviting
  external people from desktop is a separate flow (manual email
  entry + invitation endpoint) that doesn't belong here.

- IUserDirectory: central Yavsc user directory, the only consumer
  of /api/user-search. Both Desktop and Mobile providers delegate
  to UserSearchClient; the platform split exists so future
  platform-specific sources (offline cache, directory-scoped
  providers) can plug in without disturbing consumers.

ContactDto restores IReadOnlyList<string> Emails (the flat Email
from d0e0f4c1 was a regression that matched the wire shape of
/api/user-search at the cost of the mobile provider's per-contact
list). UserSummary is a separate platform-neutral record that
mirrors the server's UserSearchResultDto without leaking transport
concerns.

App.axaml.cs registers both interfaces as singletons.

Build + 51/51 PostIt.Tests green. No UI consumer yet — these
interfaces are still plomberie; the ViewModel that joins them for
the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
d0e0f4c175
feat(postit): wire Desktop address book to /api/user-search
Replaces the empty ContactService.Desktop stub with a real
implementation backed by UserSearchClient. Closes the loop
between the server-side /api/user-search endpoint (b3056f1c),
the client wrapper (6e7e0414), and the platform abstraction.

IContactService gains:
- SearchAsync(string query, CancellationToken): on desktop,
  hits /api/user-search and appends results to an in-memory
  cache. On mobile, throws PlatformNotSupportedException —
  mobile providers use the device-local address book
  (GetDeviceContactsAsync) and don't talk to a network search.
- Contacts (ObservableCollection<ContactDto>): live view of
  the cache; UI binds directly to it. Mobile populates it
  inside GetDeviceContactsAsync (eager load); desktop populates
  it via SearchAsync (lazy, on-demand).

ContactDto shape changes:
- Emails (IReadOnlyList<string>) -> Email (string?). The
  /api/user-search endpoint returns one email per user. The
  use case ('invite / add to a circle') only needs one.
- Mobile provider flattens its per-contact email list down
  to the first non-empty entry (a small functional loss that
  matches the wire shape).

App.axaml.cs constructs a ContactService from the
UserSearchClient singleton and registers it as
IContactService so future ViewModels can take the interface
by constructor injection.

Build + 51/51 tests green. The mobile provider is still
gated by #if ANDROID || IOS and not exercised by the
Desktop test target — runtime behaviour on Android will
need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
6e7e04141b
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.
2026-08-18 00:34:29 +01:00
69a660cafb
feat(app-invite): isolate ContactService to mobile targets
Splits the single ContactService class (which threw
PlatformNotSupportedException on non-Android/iOS targets) into a
platform-conditional structure:

- IContactService + ContactDto: shared abstraction in
  src/PostIt/PostIt/Services/IContactService.cs. ViewModels depend
  on this; concrete providers map their native shapes to ContactDto.

- ContactService.Mobile.cs: MAUI Essentials implementation, compiled
  only when ANDROID or IOS is defined. Wraps
  Contacts.Default.GetAllAsync() with permission handling and a
  NotImplementedInReferenceAssemblyException safety net.

- ContactService.Desktop.cs: stub returning an empty list, compiled
  when neither ANDROID nor IOS is defined. Replaces the
  'throw PlatformNotSupportedException' path so desktop targets
  (PostIt.Desktop, PostIt.Browser) build and run cleanly.

The Microsoft.Maui.Essentials portable facade is referenced from
PostIt.csproj, but it only becomes functional when the host
application project (PostIt.Android, future PostIt.iOS) also
references the platform-specific implementation.

No tests added: per AGENTS.md, a 'stub returns empty list' test on
PostIt.Tests (net10.0 desktop target) would be cosmetic and not
detect the real failure mode. Android-side tests require a working
PostIt.Android project, which doesn't exist yet.

Future providers (Google Contacts API, Exchange, CardDAV) plug in
as additional IContactService implementations selected by DI
configuration.
2026-08-18 00:31:26 +01:00
a8c219e0fa
WIP app invite: scaffold MAUI Essentials dependency in shared PostIt
Adds Microsoft.Maui.Essentials package and <UseMaui>true</UseMaui> to
src/PostIt/PostIt/PostIt.csproj so the shared project can compile code
that calls MAUI Essentials APIs (Microsoft.Maui.ApplicationModel.*).

Also adds a draft ContactService that wraps Contacts.Default.GetAllAsync()
behind a runtime platform check and permission request.

WIP caveats:
- The portable MAUI Essentials facade compiles on net10.0 but throws
  NotImplementedInReferenceAssemblyException at runtime when no
  platform-specific MAUI Essentials binary is loaded. A PostIt.Android
  project (or equivalent) must reference the Android MAUI Essentials
  implementation for Contacts.Default.GetAllAsync() to actually work.
- On desktop (Linux/macOS/Windows) the API is unsupported by design;
  ContactService currently throws PlatformNotSupportedException. A
  desktop stub returning Array.Empty<Contact>() is the likely next step.
- No tests yet. The scaffold is unverified at runtime; build passes.
2026-08-18 00:31:26 +01:00
b3056f1c2e
feat(user-search): add UserSearchApiController in Yavsc.Blogs
All checks were successful
Dotnet build and test / log-the-inputs (pull_request) Successful in 26s
Dotnet build and test / build (pull_request) Successful in 14m59s
Lives in Yavsc.Blogs (not Yavsc.Api) because Yavsc.Api is not
yet enabled in production; future migration to Yavsc.Api is a
single namespace + route prefix change.

Endpoint: GET /api/user-search?q=<name>&e=<email>&take=<n>
- Authorisation: [Authorize] (any authenticated caller).
- q: case-insensitive substring match on FullName OR UserName.
- e: case-insensitive exact match on Email.
- take: 1..100, default 25.

Returns a flat UserSearchResultDto (Id, UserName, FullName,
Avatar, Email) — no navigation properties, so the payload
stays small even if the user table grows.

The Email field is included because the address-book use case
(composing circle membership, sending invites) needs it.
On Yavsc's single-tenant deployments the user table is a
closed community; multi-tenant deployments should gate this
controller behind a tenant-scoped policy before exposing it.
The trade-off is documented in the controller's class-level
XML doc.
2026-08-18 00:20:10 +01:00
1b289c1387
refactor(model): rename Yavsc.Blogspot.BlogPost to BlogPostDto
When commit 0e95e283 moved BlogPost from PostIt.Models to
Yavsc.Blogspot, it created an unfortunate collision with the
server-side EF entity Yavsc.Models.Blog.BlogPost. The two
classes have nothing in common beyond the name; the DTO is
the wire shape PostIt exchanges with the Blogs API, the EF
entity is the persistence model. Server code that imports both
namespaces (BlogSpotService.cs, etc.) ended up with 'BlogPost
is an ambiguous reference between X and Y' errors.

Renaming the client DTO to BlogPostDto (matching the
naming convention of the other DTOs in Yavsc.Api.Client.Dtos
— CircleDto, CircleAuthorizationDto, UserSearchResultDto)
disambiguates without renaming the EF entity on the server.

The namespace stays Yavsc.Blogspot; only the class name
changes. All call sites (client code, tests, XAML DataTemplates,
XML doc comments) are updated mechanically.
2026-08-18 00:20:01 +01:00
0e7576857d
feat(postit): UI for managing Circles + per-post ACL
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 13s
Dotnet build and test / build (pull_request) Failing after 3m51s
Landing the user-facing surface for the BlogAcl work. The user
can now:

1. Open the 'Mes cercles' page (a new 'Mes cercles' button on
   the main page) and create / edit / delete their own
   circles. The page lists circles in an ObservableCollection
   bound to a ListBox; per-row buttons drive StartEdit and
   Delete; the bottom editor pushes new / edited circles via
   the Save command.

2. With a post selected, click the new 'ACL' button to open a
   modal 'PostAclDialog' for that post. The modal shows the
   current ACL entries (filtered server-side by Allowed.OwnerId
   == caller) and a dropdown of the caller's circles to add.
   Each entry has a 'Revoke' button.

Both pages follow the same pattern:
- ViewModel uses [ObservableProperty] for state and
  [RelayCommand] for verbs; IsBusy drives a ProgressBar
  overlay; StatusMessage surfaces server feedback.
- View follows the XAML-Background/Foreground lesson (no
  hard-coded colours), so dark mode works without
  contrast surprises.
- Code-behind is minimal — just AvaloniaXamlLoader.Load —
  because navigation is driven by RelayCommand + event
  (ManageAclRequested, OpenCirclesRequested) that the
  MainPage code-behind handles via its DataContextChanged
  handler.

The 'complete' scope (c) of this commit was confirmed by
Paul. Three follow-up tracks are deliberately out of scope
and tracked in MEMORY.md (2026-08-18):
- i18n: no .resx / IStringLocalizer today; all visible text
  is hard-coded French.
- Avalonia.Headless UI tests: only ViewModel-level coverage
  is feasible today; full navigation tests are a separate
  effort.
- XAML accessibility audit of pre-existing pages (Settings,
  MainPage) that predate the Background/Foreground lesson.

Build + 51/51 tests green.
2026-08-18 00:06:57 +01:00
a5887a2387
feat(postit): wire Circle + BlogAcl clients in the DI container
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Successful in 21s
Dotnet build and test / build (pull_request) Failing after 7m24s
App.axaml.cs is the composition root for PostIt. It now also
builds and registers:
- CircleApiClient (singleton) — backed by the same YavscApiClient
  and the same blogs base URL as BlogApiClient
- BlogAclApiClient (singleton) — same shape
- IYavscApiClient -> YavscApiClient mapping (singleton). The
  concrete class is still resolvable as YavscApiClient; the new
  registration makes the same instance available as
  IYavscApiClient so future consumers (and unit tests) can take
  the interface without coupling to the concrete type.

The 3 high-level clients are singletons: they hold no mutable
state of their own, just a reference to YavscApiClient and a
base URL. Reusing the same instance across requests is what the
HttpClient inside YavscApiClient was already designed for.
2026-08-17 23:51:51 +01:00
f835ad42a1
feat(api-client): add Yavsc.Api.Client with Blog + Circle + BlogAcl clients
Creates the high-level HTTP client library the PostIt UI will
consume to manage blog posts, circles, and per-post ACLs.

Clients in this commit:
- BlogApiClient (moved from PostIt/Services; same public surface,
  now depends on IYavscApiClient instead of the concrete class).
- CircleApiClient (new): GET/POST/PUT/DELETE /api/circle. Takes
  the blogs base URL explicitly in its constructor so it doesn't
  need to know about PostIt's Settings type.
- BlogAclApiClient (new): GET/POST/PUT/DELETE /api/blogacl.
  Same conventions as CircleApiClient.

DTOs (Yavsc.Api.Client.Dtos):
- CircleDto: id, name, ownerId, public. Stops short of the
  navigation properties on the server-side Circle (Owner,
  Members), which depend on ApplicationUser and other server
  types we don't want to drag into the client.
- CircleAuthorizationDto: circleId, blogPostId, comment. Same
  reason: the server entity has Target and Allowed navigation
  properties the client never needs.

The clients now require the caller to pass the blogs base URL
explicitly in the constructor (previously the BlogApiClient
sniffed it off YavscApiClient.Settings.BlogsApiUrl, but that
field is PostIt-specific). The one production call site
(App.axaml.cs) and four test call sites are updated to pass
the URL.

Build + 51/51 tests green. The IYavscApiClient abstraction was
landed in the previous commit so this one could be a pure
addition + relocation.
2026-08-17 23:50:35 +01:00
ab40af8ef1
refactor(api-client): introduce IYavscApiClient abstraction in Yavsc.Api.Client
Yavsc.Api.Client is the new home for high-level HTTP clients
(BlogApiClient, CircleApiClient, BlogAclApiClient, etc.). It
depends on the host application's transport layer, but the host
(PostIt) is a UI app with OIDC, settings, and an ApplicationData
directory — none of which the abstract client library should
know about.

The IYavscApiClient interface captures just the transport
surface those clients need:
- HttpClient (so the client can configure BaseAddress)
- CallAsync<T> and CallAsync (the JSON over HTTP verb)

It deliberately leaves out LoginAsync / TrySilentLoginAsync /
CurrentAccessToken / HasValidSession / Settings — those are
authentication and configuration concerns, not transport. They
stay on the concrete YavscApiClient in PostIt.Services.

The concrete YavscApiClient now implements IYavscApiClient; the
existing public surface is unchanged (no breaking changes for
existing call sites in PostIt or the tests).

This commit only lays the foundation. The actual high-level
clients (Blog/Circle/BlogAcl) land in a follow-up commit that
re-uses this interface, so this one stays a small, reviewable
refactor.
2026-08-17 23:50:24 +01:00
0e95e28327
refactor(model): move BlogPost DTO from PostIt.Models to Yavsc.Blogspot
BlogPost is shared between the server (Yavsc.Server/Models/Blog/
BlogPost.cs is the EF entity) and any client that talks to the
blogs API. Keeping the client-side DTO in PostIt.Models made
sense when there was only one consumer; now that the
Yavsc.Api.Client project is about to host BlogApiClient alongside
CircleApiClient and BlogAclApiClient, the DTO has to live in a
layer both the client project and PostIt can reference without
inverting the dependency.

Yavsc.Abstract is the existing home for cross-tier interfaces
and DTOs (IBlogPost, IBlogPostPayLoad, IApplicationUser).
Yavsc.Blogspot is the sub-namespace already used by the
matching interface, so the new concrete class follows.

Why not move Circle and CircleAuthorizationToBlogPost at the
same time? Both depend on the concrete ApplicationUser class
(via the Owner and Target/Allowed navigation properties) which
lives in Yavsc.Server. Moving them would mean either dragging
ApplicationUser into the abstract layer (huge blast radius —
auth, billing, chat, etc.) or weakening the navigation
properties (breaks EF Core shaping). They're staying where
they are; the new Yavsc.Api.Client will get DTO counterparts
instead.

Updated call sites:
- 4 .cs files: replace 'using PostIt.Models;' with
  'using Yavsc.Blogspot;' where the file was actually using
  BlogPost. Files that only used SignaturePadData keep their
  'using PostIt.Models;' — that type stays put.
- 1 .axaml file: xmlns:models="using:PostIt.Models" ->
  xmlns:models="using:Yavsc.Blogspot" (one DataTemplate for
  the post list in MainPage).

Build + tests green (51/51).
2026-08-17 23:45:45 +01:00
e376aed887
fix(blogacl): restrict Circle + BlogAcl reads and writes to caller's own data
Closes the data-leak holes that survived the move of these controllers
from Yavsc.Api to Yavsc.Blogs. Circles are personal — a circle and its
membership should never be visible, modifiable, or deletable by anyone
other than its owner.

BlogAclApiController:
- GetBlogACL() was returning the full table; now filters by
  Allowed.OwnerId == caller's uid, with an Include(a => a.Allowed)
  so EF Core can push the filter into SQL instead of materialising
  the whole table.
- Other endpoints (GetById, Put, Post, Delete) already enforced
  ownership; left as is.

CircleApiController:
- GetCircle() (no id) now filters by OwnerId.
- GetCircle(id) now requires c.Id == id && c.OwnerId == uid;
  returns 404 (not 403) on miss to avoid leaking the existence of
  someone else's circle.
- PutCircle verifies the existing record is owned by the caller,
  then forces circle.OwnerId = uid on the body (the client's value
  is ignored). Returns ChallengeResult when the caller doesn't own
  the record.
- PostCircle forces circle.OwnerId = uid (was trusting the body).
- DeleteCircle now filters by OwnerId; 404 on miss.

All checks use the same source of truth (User.FindFirstValue(
ClaimTypes.NameIdentifier)) that the existing BlogAclApiController
authz code already relies on.
2026-08-17 23:36:20 +01:00
40e5630cfc
refactor(blogacl): move BlogAcl + Circle controllers from Yavsc.Api to Yavsc.Blogs
These two controllers belong to the Blogs subsystem (their routes
/api/blogacl and /api/circle are blog-domain concerns, not the
generic Api surface). Moving them next to BlogApiController keeps
related code together and prepares the PostIt client to consume
them through the same BlogsApiUrl base address as the existing
BlogApiClient.

Mechanical changes only:
- Namespace Yavsc.Controllers -> Yavsc.Blogs.Controllers
- Drop unused 'using Yavsc.Helpers;' (no symbol in the new
  compilation unit depends on it; the build confirms it was
  dead since the controllers were first written)
- Fix typo in CircleApiController route: 'api/cirle' -> 'api/circle'
  (any client trying to call the documented route was hitting 404)

No functional changes to authorization or query shape. The known
security gaps in these controllers (GetBlogACL and GetCircle
return unfiltered collections, DeleteCircle has no ownership
check) are deliberately left untouched in this commit and will
be addressed in a follow-up.
2026-08-17 23:34:48 +01:00
7f84d4d97a
fix(billing): tolerate ReflectionTypeLoadException during init
ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies()
and calls Assembly.GetTypes() on each. If any of the loaded assemblies
has a type that fails to resolve (a flaky dependency, an AddOn with a
broken reference, a test dependency that's been rewritten after compile),
GetTypes() throws ReflectionTypeLoadException (or, less commonly,
FileNotFoundException / TypeLoadException for the assembly itself).

In CI on the forgejo-runner (and especially in test discovery under
xunit v3), one such assembly is loaded somewhere between test runs and
silently throws. The exception is not handled, so:

  1. Collections are Cleared at the top of ConfigureBillingService().
  2. The reflection loop throws before reaching the
     RegisterBilling<HairCutQuery/HairMultiCutQuery/RdvQuery> calls.
  3. BillingService.Billing ends up empty (Count = 0).
  4. The second ConfigureBillingService() call sees the same assembly
     loaded (xunit v3 keeps the AppDomain warm for the whole suite),
     throws identically, and the test
     Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing
     fails with 'Assert.Equal() Failure: Expected 3, Actual 0'.

Fix: catch ReflectionTypeLoadException and use the partial
.Types() list (the successfully-resolved subset), and use a
broader catch (with continue) for any other assembly-level
load failure. The lost user-settings types are not material;
they are derived from ApplicationDbContext in a separate loop
right after, and the RegisterBilling<>() calls that populate
BillingService.Billing run last, after both reflective phases
have completed best-effort.

The test still passes locally because the local test environment
loads a clean set of assemblies; only the CI runner (with its
extra test-time tooling) hits this path.
2026-08-16 16:35:05 +01:00
eebc83cf7e
Add comments API support and tests
Some checks failed
Dotnet build and test / build (push) Has been cancelled
Dotnet build and test / log-the-inputs (push) Has been cancelled
2026-08-10 22:27:52 +01:00
0fd9e40d67
Fix blog comment endpoint path and add regression test
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 21:48:03 +01:00
0d3fbf22c3
GetUserId_reads_NameIdentifier_when_sub_was_mapped
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 18:34:01 +01:00
44b391d496
Activity protection
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 18:12:59 +01:00
cd03b04755
re-refacto BlogPost serialization
Some checks failed
Dotnet build and test / build (push) Has been cancelled
Dotnet build and test / log-the-inputs (push) Has been cancelled
2026-08-05 21:11:22 +01:00
64547840e4
refacto BlogPost serialization
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-05 21:05:14 +01:00
7d1cca9df0
build
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-03 02:16:57 +01:00
3744d9ae9c
Enable blogs on connected status
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-03 01:48:15 +01:00
b25e0e842e
refacto blogPost
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-03 01:36:12 +01:00
3b21a12c20 Le créateur vient de l'authentification, donc on ne le prend pas du post
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-02 23:02:54 +01:00
286c29f4e3 Navigate to Main Page
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-02 21:08:25 +01:00
786016344b refacto error handling
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 17:56:23 +01:00
fda43ba2d1 Hsts 2026-07-12 16:27:13 +01:00
3d80a3f2a1 Post logout redirect uri 2026-07-12 16:14:37 +01:00
13964b9f7f org: show full error details in development
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 16:07:28 +01:00
eba44b46e2 postit: allow self-signed OIDC TLS in Development
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 15:51:55 +01:00
b827f531aa Localisation
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 15:18:07 +01:00
5eb8019beb Audiences 2026-07-12 15:17:53 +01:00
0f73728b85 code format 2026-07-12 15:17:41 +01:00
9f061277c7 auth: fix JWT default scheme and multi-audience validation
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-07-12 14:53:17 +01:00
b1e8d37f21 blogs: use centralized JWT audience handling
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 06:45:00 +01:00
1b0933c215 tests: log warning when OIDC token fallback is used 2026-07-12 06:43:07 +01:00
6e4b68c60d Merge remote-tracking branch 'origin/main' into fic/jwt-validation 2026-07-12 06:12:19 +01:00
7fa55a68c9 WIP audiences 2026-07-12 06:10:24 +01:00
6c100ff759 use an available port for authority
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 06:01:42 +01:00
4aacaf5e51 tests: configure static fixture ports and update org test config 2026-07-12 05:48:47 +01:00
713f66f1a7 ? 2026-07-12 04:36:35 +01:00
09604092c5 Revert "Test host: bind Kestrel to Site:Authority instead of dynamic port"
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
This reverts commit 402bcc1db8.
2026-07-12 03:48:49 +01:00
1ab0bef9a4 using clauses cleanup 2026-07-12 03:48:30 +01:00
402bcc1db8 Test host: bind Kestrel to Site:Authority instead of dynamic port
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
The Yavsc.Org integration tests were failing 'Internal Server Error' on
the OIDC discovery document when run as part of the full test suite.

Root cause: WebHostFixture bound Kestrel to IPAddress.Loopback on a
dynamically-allocated port and exposed it via IServerAddressesFeature.
But the OIDC issuer URLs (and the issuer claim) come from Site:Authority,
which was left at the production value (mercure.pschneider.fr). So
IdentityServer8's discovery document advertised URLs unreachable from
the test process, and the discovery call returned a 500.

Fix:
  - WebServerFixture now overrides Site:Authority and Site:ExternalUrl
    in AddInMemoryCollection to 'https://localhost:44300' (the ASP.NET
    Core dev HTTPS convention).
  - WebHostFixture reads Site:Authority from configuration and binds
    Kestrel to that fixed URL. The exposed Addresses list is sourced
    from the same configuration value instead of the
    IServerAddressesFeature, so the listen URL and the OIDC issuer
    URLs always match.

Remoting.cs (Mandatory/Remoting.cs): add 'using
Microsoft.Extensions.DependencyInjection;' so the existing OIDC/DB
diagnostic block (capture raw HTTP response + dump OIDC-related DB
state on discovery failure) compiles. The diagnostic itself is left
in place — it's what surfaced the 500 in the first place.
2026-07-12 03:43:22 +01:00
f3bb039d2f WIP audiences 2026-07-12 02:42:13 +01:00
9846210fd6 ApplicationDbContext: drop redundant HasOne on 3 Client navs
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
LoadClientAsync(id) on ClientController used to trip an
IndexOutOfRangeException at the InMemory shaper for any
.Include() of one of three Client navs: RedirectUris,
AllowedScopes, AllowedGrantTypes. Five other Client navs (with
the same EF shape and the same application-level config) worked
fine.

Bisection pointed at the InMemory provider; that hypothesis was
wrong. The real cause is in ApplicationDbContext.OnModelCreating:
yavsc was redeclaring the HasOne<Client>().WithMany(...).
HasForeignKey(e => e.ClientId) for all eight Client* navs. The
same relation is already declared (more completely, with
.IsRequired().OnDelete(DeleteBehavior.Cascade)) by
IdentityServer8's ConfigureClientStore via ModelBuilderExtensions.

The redundant mapping on three specific entities — ClientScope,
ClientRedirectUri, ClientGrantType — interacts with the InMemory
provider's shaper in a way that throws IndexOutOfRange. Removing
the redundancy fixes it.

This commit also walks back b12c272d:
- Drops .AsSplitQuery() from LoadClientAsync (no longer needed
  for the InMemory shaper, and the Postgres path it was a hedge
  against was a false alarm — there is no Postgres production
  bug here, only an InMemory shaper quirk surfaced by the
  redundant mapping).
- Removes the 9 Bisect_*_alone tests that were the artefact of
  the provider-hypothesis phase. They pointed at the right
  entities but for the wrong reason.
- Keeps EditRedirectUris_GET_after_add_lists_both_uris as the
  end-to-end regression sentinel: with the fix in place, it
  loads a Client with two RedirectUris and asserts both are
  rendered. Without the fix, it fails with IndexOutOfRange.
2026-07-12 02:39:13 +01:00
b12c272df7 ClientController: split LoadClientAsync into per-collection subqueries
LoadClientAsync chains 9 .Include() calls on dbContext.Clients.
On Postgres (and InMemory for some IdentityServer8 nav types), the
resulting cartesian product trips the query shaper with
IndexOutOfRangeException at IncludeCollection materialisation time.
Bug reproduces in production on the Blog admin pages that load a
Client by id.

AsSplitQuery() rewrites the load as 9 separate SELECTs joined by
client id, which sidesteps the cartesian explosion and any shaper
ambiguity between Claims/Properties/ClientSecrets (which share
Type/Value column names across some IdentityServer8 versions).

Tests:
- EditRedirectUris_GET_after_add_lists_both_uris: end-to-end
  reproducer that adds a second RedirectUri via POST then re-GETs
  the editor. Guards the fix on the integration path.
- Bisect_*_alone: nine unit tests that exercise the same
  .SingleOrDefaultAsync(c => c.Id == id).Include(nav) on the
  InMemory provider, one nav at a time. Pinpointed three
  problematic navs (RedirectUris, AllowedScopes, AllowedGrantTypes)
  on InMemory; kept as a regression net for any future shaper
  regressions on the InMemory provider (not the Postgres path).
2026-07-12 01:15:02 +01:00
cb20b8a2d5 Revert "repoduces the bug"
This reverts commit fa7794b7a0.
2026-07-11 22:17:58 +01:00
Lum
fa7794b7a0 repoduces the bug 2026-07-11 21:56:25 +01:00