From 231b0ac9a0273ac5c41e15419fb6a3b5f491c712 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 3 May 2026 00:08:47 +0200 Subject: [PATCH] Remove workspace session-token coupling --- RpgRoller/Components/App.razor | 6 +- RpgRoller/Components/WorkspaceQueryService.cs | 65 ++++++++----------- .../WorkspaceSessionTokenAccessor.cs | 33 ---------- RpgRoller/Program.cs | 2 - tests/e2e/smoke.spec.js | 28 ++++++++ 5 files changed, 58 insertions(+), 76 deletions(-) delete mode 100644 RpgRoller/Components/WorkspaceSessionTokenAccessor.cs diff --git a/RpgRoller/Components/App.razor b/RpgRoller/Components/App.razor index 49c232a..697b61e 100644 --- a/RpgRoller/Components/App.razor +++ b/RpgRoller/Components/App.razor @@ -17,7 +17,7 @@ @if (UseInteractiveApp) { - + } @@ -27,7 +27,7 @@ } else { - + } @if (UseInteractiveApp) @@ -92,4 +92,4 @@ else return value.Count > 0 ? value[0] : null; } -} \ No newline at end of file +} diff --git a/RpgRoller/Components/WorkspaceQueryService.cs b/RpgRoller/Components/WorkspaceQueryService.cs index f588293..4661fb4 100644 --- a/RpgRoller/Components/WorkspaceQueryService.cs +++ b/RpgRoller/Components/WorkspaceQueryService.cs @@ -1,81 +1,70 @@ +using Microsoft.AspNetCore.WebUtilities; using RpgRoller.Contracts; -using RpgRoller.Services; namespace RpgRoller.Components; -public sealed class WorkspaceQueryService(IGameService gameService, WorkspaceSessionTokenAccessor sessionTokenAccessor) +public sealed class WorkspaceQueryService(RpgRollerApiClient apiClient) { public Task GetMeAsync() { - return Task.FromResult(GetValue(gameService.GetMe(GetRequiredSessionToken()))); + return apiClient.RequestAsync("GET", "/api/me"); } - public Task> GetRulesetsAsync() + public async Task> GetRulesetsAsync() { - return Task.FromResult(gameService.GetRulesets()); + return await apiClient.RequestAsync("GET", "/api/rulesets"); } - public Task> GetCampaignsAsync() + public async Task> GetCampaignsAsync() { - return Task.FromResult(GetValue(gameService.GetCampaigns(GetRequiredSessionToken()))); + return await apiClient.RequestAsync("GET", "/api/campaigns"); } - public Task> GetCharacterCampaignOptionsAsync() + public async Task> GetCharacterCampaignOptionsAsync() { - return Task.FromResult(GetValue(gameService.GetCharacterCampaignOptions(GetRequiredSessionToken()))); + return await apiClient.RequestAsync("GET", "/api/campaigns/options"); } public Task GetCampaignAsync(Guid campaignId) { - return Task.FromResult(GetValue(gameService.GetCampaign(GetRequiredSessionToken(), campaignId))); + return apiClient.RequestAsync("GET", $"/api/campaigns/{campaignId:D}"); } - public Task> GetUsernamesAsync() + public async Task> GetUsernamesAsync() { - return Task.FromResult(GetValue(gameService.GetUsernames(GetRequiredSessionToken()))); + return await apiClient.RequestAsync("GET", "/api/users/usernames"); } public Task GetCharacterSheetAsync(Guid characterId) { - return Task.FromResult(GetValue(gameService.GetCharacterSheet(GetRequiredSessionToken(), characterId))); + return apiClient.RequestAsync("GET", $"/api/characters/{characterId:D}/sheet"); } - public Task> GetCampaignLogAsync(Guid campaignId) + public async Task> GetCampaignLogAsync(Guid campaignId) { - return Task.FromResult(GetValue(gameService.GetCampaignLog(GetRequiredSessionToken(), campaignId))); + return await apiClient.RequestAsync("GET", $"/api/campaigns/{campaignId:D}/log"); } public Task GetCampaignLogPageAsync(Guid campaignId, Guid? afterRollId = null, int? limit = null) { - return Task.FromResult(GetValue(gameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit))); + var query = new Dictionary(); + if (afterRollId.HasValue) + query["afterRollId"] = afterRollId.Value.ToString("D"); + + if (limit.HasValue) + query["limit"] = limit.Value.ToString(); + + var path = QueryHelpers.AddQueryString($"/api/campaigns/{campaignId:D}/log/page", query); + return apiClient.RequestAsync("GET", path); } public Task GetRollDetailAsync(Guid rollId) { - return Task.FromResult(GetValue(gameService.GetRollDetail(GetRequiredSessionToken(), rollId))); + return apiClient.RequestAsync("GET", $"/api/rolls/{rollId:D}"); } - public Task> GetAdminUsersAsync() + public async Task> GetAdminUsersAsync() { - return Task.FromResult(GetValue(gameService.GetUsers(GetRequiredSessionToken()))); - } - - private string GetRequiredSessionToken() - { - return sessionTokenAccessor.GetRequiredSessionToken(); - } - - private static T GetValue(ServiceResult result) - { - if (result.Succeeded) - return result.Value!; - - throw ToApiRequestException(result.Error!); - } - - private static ApiRequestException ToApiRequestException(ServiceError error) - { - var statusCode = error.Code == "unauthorized" ? 401 : 400; - return new(statusCode, error.Message, error.Code); + return await apiClient.RequestAsync("GET", "/api/admin/users"); } } \ No newline at end of file diff --git a/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs b/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs deleted file mode 100644 index 81fc55f..0000000 --- a/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using RpgRoller.Api; - -namespace RpgRoller.Components; - -public sealed class WorkspaceSessionTokenAccessor -{ - public WorkspaceSessionTokenAccessor(IHttpContextAccessor httpContextAccessor) - { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext is null) - return; - - if (httpContext.Items.TryGetValue(SessionTokenItemKey, out var storedToken) && storedToken is string sessionToken && !string.IsNullOrWhiteSpace(sessionToken)) - { - m_SessionToken = sessionToken; - return; - } - - if (httpContext.TryReadSessionTokenFromCookie(out sessionToken)) - m_SessionToken = sessionToken; - } - - public string GetRequiredSessionToken() - { - if (!string.IsNullOrWhiteSpace(m_SessionToken)) - return m_SessionToken; - - throw new ApiRequestException(401, "You must be logged in."); - } - - private const string SessionTokenItemKey = "__rpgroller.session-token"; - private readonly string? m_SessionToken; -} \ No newline at end of file diff --git a/RpgRoller/Program.cs b/RpgRoller/Program.cs index ee74e04..1a912c9 100644 --- a/RpgRoller/Program.cs +++ b/RpgRoller/Program.cs @@ -13,9 +13,7 @@ builder.Services.AddResponseCompression(options => options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]); }); builder.Services.ConfigureHttpJsonOptions(options => RpgRollerJson.Configure(options.SerializerOptions)); -builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); var app = builder.Build(); diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js index c8268f7..06e2040 100644 --- a/tests/e2e/smoke.spec.js +++ b/tests/e2e/smoke.spec.js @@ -44,6 +44,34 @@ test("home document renders static auth markup without bootstrapping blazor", as expect(html).toContain("data-auth-page"); }); +test("authenticated home document avoids prerendered workspace shell", async ({ request }) => { + const username = `doc-auth-${Date.now()}`; + const password = "Password123"; + + await postJson(request, "/api/auth/register", { + username, + password, + displayName: "Document Auth" + }); + + const loginResponse = await request.post("/api/auth/login", { + data: { + username, + password + } + }); + expect(loginResponse.ok()).toBeTruthy(); + + const response = await request.get("/"); + expect(response.ok()).toBeTruthy(); + + const html = await response.text(); + expect(html).toContain("_framework/blazor.web.js"); + expect(html).not.toContain("Register or log in to join a campaign session."); + expect(html).not.toContain("Loading user..."); + expect(html).not.toContain("Offline fallback"); +}); + test("successful login transitions to play workspace", async ({ page, context }) => { const username = `login-${Date.now()}`; const password = "Password123";