Move workspace reads server-side

This commit is contained in:
2026-04-01 23:41:03 +02:00
parent 001f775714
commit 107b8b8552
6 changed files with 235 additions and 27 deletions

View File

@@ -58,31 +58,16 @@ public partial class Workspace : IAsyncDisposable
private async Task CheckHealthAsync()
{
try
{
var health = await ApiClient.RequestAsync<HealthResponse>("GET", "/api/health");
if (!string.Equals(health.Status, "ok", StringComparison.OrdinalIgnoreCase))
{
HasHealthIssue = true;
HealthIssueMessage = "Health endpoint returned an unhealthy response.";
return;
}
HasHealthIssue = false;
HealthIssueMessage = string.Empty;
}
catch (ApiRequestException)
{
HasHealthIssue = true;
HealthIssueMessage = "Unable to reach API. Retry to continue.";
}
HasHealthIssue = false;
HealthIssueMessage = string.Empty;
await Task.CompletedTask;
}
private async Task LoadRulesetsAsync()
{
try
{
Rulesets = (await ApiClient.RequestAsync<IReadOnlyList<RulesetDefinition>>("GET", "/api/rulesets")).ToList();
Rulesets = (await WorkspaceQuery.GetRulesetsAsync()).ToList();
}
catch (ApiRequestException ex)
{
@@ -94,7 +79,7 @@ public partial class Workspace : IAsyncDisposable
{
try
{
var usernames = await ApiClient.RequestAsync<IReadOnlyList<string>>("GET", "/api/users/usernames");
var usernames = await WorkspaceQuery.GetUsernamesAsync();
KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList();
}
catch (ApiRequestException ex)
@@ -133,7 +118,7 @@ public partial class Workspace : IAsyncDisposable
{
try
{
return await ApiClient.RequestAsync<MeResponse>("GET", "/api/me");
return await WorkspaceQuery.GetMeAsync();
}
catch (ApiRequestException ex) when (ex.StatusCode == 401)
{
@@ -143,7 +128,7 @@ public partial class Workspace : IAsyncDisposable
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
{
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignSummary>>("GET", "/api/campaigns");
var campaigns = await WorkspaceQuery.GetCampaignsAsync();
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
if (Campaigns.Count == 0)
@@ -164,7 +149,7 @@ public partial class Workspace : IAsyncDisposable
private async Task ReloadCharacterCampaignOptionsAsync()
{
var campaignOptions = await ApiClient.RequestAsync<IReadOnlyList<CampaignOption>>("GET", "/api/campaigns/options");
var campaignOptions = await WorkspaceQuery.GetCharacterCampaignOptionsAsync();
CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
}
@@ -185,7 +170,7 @@ public partial class Workspace : IAsyncDisposable
try
{
var campaignId = SelectedCampaignId.Value;
SelectedCampaign = await ApiClient.RequestAsync<CampaignRoster>("GET", $"/api/campaigns/{campaignId}");
SelectedCampaign = await WorkspaceQuery.GetCampaignAsync(campaignId);
SyncSelectedCharacter();
if (IsPlayScreen && PlaySelectedCharacterId.HasValue && SelectedCharacterId != PlaySelectedCharacterId)
@@ -194,7 +179,7 @@ public partial class Workspace : IAsyncDisposable
await RefreshSelectedCharacterSheetAsync();
CampaignLog = IsPlayScreen
? (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList()
? (await WorkspaceQuery.GetCampaignLogAsync(campaignId)).ToList()
: [];
await EnsureSelectedCharacterActiveAsync();
@@ -320,7 +305,7 @@ public partial class Workspace : IAsyncDisposable
private async Task ReloadAdminUsersAsync()
{
AdminUsers = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users"))
AdminUsers = (await WorkspaceQuery.GetAdminUsersAsync())
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -586,7 +571,7 @@ public partial class Workspace : IAsyncDisposable
return;
}
var sheet = await ApiClient.RequestAsync<CharacterSheet>("GET", $"/api/characters/{SelectedCharacterId.Value}/sheet");
var sheet = await WorkspaceQuery.GetCharacterSheetAsync(SelectedCharacterId.Value);
SelectedCharacterSkillGroups = sheet.SkillGroups
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -956,6 +941,9 @@ public partial class Workspace : IAsyncDisposable
[Inject]
private RpgRollerApiClient ApiClient { get; set; } = null!;
[Inject]
private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
[Inject]
private NavigationManager Navigation { get; set; } = null!;

View File

@@ -0,0 +1,80 @@
using RpgRoller.Contracts;
using RpgRoller.Services;
namespace RpgRoller.Components;
public sealed class WorkspaceQueryService
{
public WorkspaceQueryService(IGameService gameService, WorkspaceSessionTokenAccessor sessionTokenAccessor)
{
m_GameService = gameService;
m_SessionTokenAccessor = sessionTokenAccessor;
}
public Task<MeResponse> GetMeAsync()
{
return Task.FromResult(GetValue(m_GameService.GetMe(GetRequiredSessionToken())));
}
public Task<IReadOnlyList<RulesetDefinition>> GetRulesetsAsync()
{
return Task.FromResult(m_GameService.GetRulesets());
}
public Task<IReadOnlyList<CampaignSummary>> GetCampaignsAsync()
{
return Task.FromResult(GetValue(m_GameService.GetCampaigns(GetRequiredSessionToken())));
}
public Task<IReadOnlyList<CampaignOption>> GetCharacterCampaignOptionsAsync()
{
return Task.FromResult(GetValue(m_GameService.GetCharacterCampaignOptions(GetRequiredSessionToken())));
}
public Task<CampaignRoster> GetCampaignAsync(Guid campaignId)
{
return Task.FromResult(GetValue(m_GameService.GetCampaign(GetRequiredSessionToken(), campaignId)));
}
public Task<IReadOnlyList<string>> GetUsernamesAsync()
{
return Task.FromResult(GetValue(m_GameService.GetUsernames(GetRequiredSessionToken())));
}
public Task<CharacterSheet> GetCharacterSheetAsync(Guid characterId)
{
return Task.FromResult(GetValue(m_GameService.GetCharacterSheet(GetRequiredSessionToken(), characterId)));
}
public Task<IReadOnlyList<CampaignLogEntry>> GetCampaignLogAsync(Guid campaignId)
{
return Task.FromResult(GetValue(m_GameService.GetCampaignLog(GetRequiredSessionToken(), campaignId)));
}
public Task<IReadOnlyList<AdminUserSummary>> GetAdminUsersAsync()
{
return Task.FromResult(GetValue(m_GameService.GetUsers(GetRequiredSessionToken())));
}
private string GetRequiredSessionToken()
{
return m_SessionTokenAccessor.GetRequiredSessionToken();
}
private static T GetValue<T>(ServiceResult<T> 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 ApiRequestException(statusCode, error.Message);
}
private readonly IGameService m_GameService;
private readonly WorkspaceSessionTokenAccessor m_SessionTokenAccessor;
}

View File

@@ -0,0 +1,35 @@
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;
}

View File

@@ -5,7 +5,10 @@ using RpgRoller.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<RpgRollerApiClient>();
builder.Services.AddScoped<WorkspaceSessionTokenAccessor>();
builder.Services.AddScoped<WorkspaceQueryService>();
var app = builder.Build();
app.InitializeRpgRollerState();