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";