From bd9c67fc65767e24606e56307dc7d20d2ed664b6 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 19 Jun 2026 23:12:26 -0400 Subject: [PATCH 1/2] fix: capture upload-form user id at init, not submit, so token expiry mid-session can't discard a composed release --- .../Components/Pages/Tracks/BatchUpload.razor | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor index 524d089..da9bb92 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor @@ -129,6 +129,12 @@ // Set true once the admin has acknowledged the missing-hero warning, so a second submit proceeds. private bool _heroWarningAcknowledged; + // Captured at component initialization (when the token is known-good) so a mid-session + // token expiry at submit time cannot discard a long-composed release. Only assigned when + // the id parses successfully — a prerender pass returns an anonymous principal (no JS + // interop available), so the field stays null until the live interactive circuit populates it. + private long? _createdByUserId; + private string _albumName = string.Empty; private string _artist = string.Empty; private string _genre = string.Empty; @@ -156,6 +162,20 @@ } } + protected override async Task OnInitializedAsync() + { + // Capture the user id once at load, while the token is known-good. The page is [Authorize]-gated + // so this should always succeed on the live interactive circuit. During static prerender, auth + // state is anonymous (no JS interop → no localStorage JWT read), so the parse fails and the + // field stays null; the interactive circuit then runs this again and sets the real value. + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (long.TryParse(userIdValue, out var userId)) + { + _createdByUserId = userId; + } + } + // Switching to a single-track medium collapses any multi-track selection to the first row so the // single-track invariant holds before submit. The predicate reads the same MediumRules cardinality // declaration the upload service enforces, so the form and the domain cannot drift. @@ -275,13 +295,12 @@ } } - var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!long.TryParse(userIdValue, out var createdByUserId)) + if (_createdByUserId is not long createdByUserId) { - // The page is gated by [Authorize] under the Admin role, so a missing or - // unparseable id here is a configuration bug, not normal client state. - Logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue); + // _createdByUserId is set at component initialization from the authenticated principal. + // A null here means the id was unavailable even at load — a genuine configuration bug, + // since the page is [Authorize]-gated. + Logger.LogError("User id was not captured at initialization — NameIdentifier claim missing or unparseable."); _errorMessage = "Your session is missing a valid identifier. Please sign in again."; return; } From a30d15f79ddb57a96c2b39b6b37d90cb41ed8038 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 19 Jun 2026 23:23:16 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20correct=20BatchUpload=20comments=20?= =?UTF-8?q?=E2=80=94=20no=20prerender=20pass=20on=20this=20host,=20single?= =?UTF-8?q?=20init=20pass=20on=20live=20interactive=20circuit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Pages/Tracks/BatchUpload.razor | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor index da9bb92..21421be 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor @@ -129,10 +129,9 @@ // Set true once the admin has acknowledged the missing-hero warning, so a second submit proceeds. private bool _heroWarningAcknowledged; - // Captured at component initialization (when the token is known-good) so a mid-session - // token expiry at submit time cannot discard a long-composed release. Only assigned when - // the id parses successfully — a prerender pass returns an anonymous principal (no JS - // interop available), so the field stays null until the live interactive circuit populates it. + // Captured once at component initialization on the live interactive circuit, while the token + // is known-good, so a mid-session token expiry at submit time cannot discard a long-composed + // release. Only assigned when the id parses successfully. private long? _createdByUserId; private string _albumName = string.Empty; @@ -164,10 +163,9 @@ protected override async Task OnInitializedAsync() { - // Capture the user id once at load, while the token is known-good. The page is [Authorize]-gated - // so this should always succeed on the live interactive circuit. During static prerender, auth - // state is anonymous (no JS interop → no localStorage JWT read), so the parse fails and the - // field stays null; the interactive circuit then runs this again and sets the real value. + // Capture the user id once at load, while the token is known-good. The CMS host runs with + // prerender: false (InteractiveServer), so this is the single init pass — auth state is + // fully available. The page is [Authorize]-gated, so the parse should always succeed. var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier); if (long.TryParse(userIdValue, out var userId))