Client PostIt:
- JoinedRooms, SelectedRoom, messages et utilisateurs par salle
(FocusedMessages/FocusedUsers), OnRoomJoined alimente depuis la
valeur de retour du Join
- ChatPage : ComboBox de choix de la salle en focus, listes bindees
sur les vues de focus
Serveur:
- AddSignalR().AddJsonProtocol(IncludeFields=true) : ChatRoomInfo
n'expose que des champs publics, STJ ne les serialisait pas et le
client recevait des objets vides (JoinedRooms jamais alimente)
ChatUserNames n'etait jamais alimente (relique d'un SetUserName disparu),
ce qui faisait lever KeyNotFoundException a chaque connexion SignalR et
fermait immediatement la connexion cote client ("Déconnecté (The server
closed the connection...)" dans PostIt).
- IConnexionManager.OnConnected prend desormais le userName en parametre
- HubConnectionManager.OnConnected alimente ChatUserNames et ChatCxIds
- ChatHub.OnConnectedAsync passe le userName resolu depuis les claims
Inclut aussi les changements PostIt lies (bandeau de statut avec
commande d'action, handlers Reconnecting/Reconnected/Closed).
DeleteUserDirOrFile (and DeleteUserFile) read FileInfo.Length AFTER
fi.Delete(), and FileInfo.Length throws FileNotFoundException once
the file is gone. The controller's catch turned that into a 400, so
the user saw "le fichier n'existe pas sur le serveur" while the delete
had in fact succeeded. Capture the length before deleting.
Adds FileSystemApiTests.Delete_fs_removes_the_file_and_returns_200
(asserts 200 + file gone from disk). 8/8 FileSystemApiTests pass.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The action buttons inside ItemsControl item templates (Télécharger /
Supprimer / Joindre on MyFiles; download/detach on the attachment
lists of BillingQueryDetailsPage and EstimateEditionPage) were bound
with $parent[vm:XxxViewModel].Cmd, which searches for an ancestor
CONTROL of type XxxViewModel — a viewmodel, not a control — so the
binding resolved to null and the Button stayed disabled (grayed) and
inert on click. Switch to the working CirclesPage/EstimateListPage
syntax: $parent[ContentPage].((vm:XxxViewModel)DataContext).Cmd.
Adds MyFilesPageHeadlessTests: mounts MyFilesPage via NavigationPage
+ Window.Show() and asserts each row button's Command is bound,
armed (CanExecute), and fires the API call on click. 136/136 pass.
Co-Authored-By: Claude Code <noreply@anthropic.com>
GET api/v1/fs passed User.GetUserId() (the "sub" claim — a GUID in
production) to GetUserFiles, so the listing looked under
{Blog}/{sub-GUID}. But the personal-storage tree is keyed by the
user's login everywhere else: EnsureDestinationDirectory writes under
{Blog}/{name-claim}, the download endpoint and avatar paths use
user.UserName. So once the upload path was fixed (previous commit),
files landed under {Blog}/{login} while the listing still looked under
{Blog}/{GUID} — an empty tree — hence the PostIt "Mes fichiers" page
listing nothing.
GetDir now resolves the user from the DB (as Post already does) and
lists under user.UserName. UploadedFile.OwnerId stays "sub", so
EnrichWithFileIds still queries by uid.
Tests:
- TestTokenIssuer.Issue gains an optional `name` param (defaults to
subject, preserving existing behavior) so a test token can mirror
production where sub (GUID) ≠ name (login).
- FileSystemApiTests.Get_fs_lists_files_stored_under_the_user_login
decouples sub from the login, uploads under the login, and asserts
GET /api/v1/fs returns the file (and the subject-named directory does
NOT exist). Verified to fail when the controller uses uid for the
listing.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The custom ProfileService (Yavsc.Services.ProfileService) was never
registered with IdentityServer, so IS8 fell back to its default
profile service — which only emits claims listed in each API
resource's UserClaims (RequestedClaimTypes). "name" was never among
them, so every access token lacked "name" and GetUserName() resolved
to null on all bearer resource hosts (Blogs/Api): the MyFiles upload
500 and the unreachable User.Identity.Name. The class was dead code;
all prior tweaks to it (FullName→UserName) and to ServiceExtensions
(MapInboundClaims=false) were necessary but insufficient until it was
actually wired in.
- HostingExtensions: AddProfileService<ProfileService>() after
AddAspNetIdentity<ApplicationUser>().
- ProfileService: emit name=user.UserName, email, roles for any user
grant whose client has "profile" in AllowedScopes, independent of
the caller (the old ParsedScopes gate only fired for the id-token
caller, never the access-token caller).
- ServiceExtensions: JwtBearer NameClaimType=JwtClaimTypes.Name,
MapInboundClaims=false (keep short claim names as-emitted).
- FileSystemHelpers: EnsureDestinationDirectory uses GetUserName()
(the "name" claim) instead of User.Identity.Name for the per-user
storage root.
Tests:
- TestTokenIssuer: stamp a "name" claim = subject so test tokens mirror
production shape (fixes FileSystemApiTests upload 500).
- Remoting: PasswordToken_carries_name_claim_equal_to_login mints a
real JWT via the IS token endpoint (password grant, openid profile)
and asserts the access token carries name=login and GetUserName()
resolves to it.
- WebServerFixture: AccessTokenType=0 (Jwt in IS8; 1=Reference, the
inverse of Duende); allow openid+profile on the test client; re-seed
the openid/profile identity resources in ReseedAuthTestData (a
ResetAndMigrateDatabase wipes the shared in-memory store and previously
left them gone → invalid_scope).
- BillingServiceTests: [Collection("Yavsc Server")] to serialize with
the host build that mutates the static BillingService/Config
registries (pre-existing parallel race that the new test's timing
surfaced).
Co-Authored-By: Claude Code <noreply@anthropic.com>