From bd9c67fc65767e24606e56307dc7d20d2ed664b6 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 19 Jun 2026 23:12:26 -0400 Subject: [PATCH] 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; }