Add payload serializer guardrails

This commit is contained in:
2026-04-02 00:24:34 +02:00
parent ddb57cde8f
commit e028ad472d
9 changed files with 253 additions and 9 deletions

View File

@@ -1236,7 +1236,7 @@ public partial class Workspace : IAsyncDisposable
private const string CampaignSessionKey = "campaign";
private const string MobilePanelSessionKey = "play-panel";
private const string RollVisibilitySessionKey = "roll-visibility";
private const int CampaignLogWindowSize = 50;
private const int CampaignLogWindowSize = 25;
private const int ToastDurationMs = 3200;
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);

View File

@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.JSInterop;
using RpgRoller.Contracts;
namespace RpgRoller.Components;
@@ -37,7 +38,7 @@ public sealed class RpgRollerApiClient
throw new ApiRequestException(response.Status, response.Error ?? "Request failed.");
}
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private static readonly JsonSerializerOptions JsonOptions = RpgRollerJson.CreateSerializerOptions();
private readonly IJSRuntime m_Js;
}
@@ -49,4 +50,4 @@ public sealed class ApiRequestException : Exception
}
public int StatusCode { get; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json;
namespace RpgRoller.Contracts;
public static class RpgRollerJson
{
public static JsonSerializerOptions CreateSerializerOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
Configure(options);
return options;
}
public static void Configure(JsonSerializerOptions options)
{
if (!options.TypeInfoResolverChain.Contains(RpgRollerJsonSerializerContext.Default))
options.TypeInfoResolverChain.Insert(0, RpgRollerJsonSerializerContext.Default);
}
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace RpgRoller.Contracts;
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(ApiError))]
[JsonSerializable(typeof(AdminUserSummary))]
[JsonSerializable(typeof(AdminUserSummary[]))]
[JsonSerializable(typeof(CampaignGmSummary))]
[JsonSerializable(typeof(CampaignLogEntry))]
[JsonSerializable(typeof(CampaignLogEntry[]))]
[JsonSerializable(typeof(CampaignLogListEntry))]
[JsonSerializable(typeof(CampaignLogListEntry[]))]
[JsonSerializable(typeof(CampaignLogPage))]
[JsonSerializable(typeof(CampaignOption))]
[JsonSerializable(typeof(CampaignOption[]))]
[JsonSerializable(typeof(CampaignRollDetail))]
[JsonSerializable(typeof(CampaignRoster))]
[JsonSerializable(typeof(CampaignStateSnapshot))]
[JsonSerializable(typeof(CampaignSummary))]
[JsonSerializable(typeof(CampaignSummary[]))]
[JsonSerializable(typeof(CharacterSheet))]
[JsonSerializable(typeof(CharacterSheetSkill))]
[JsonSerializable(typeof(CharacterSheetSkillGroup))]
[JsonSerializable(typeof(CharacterStateVersion))]
[JsonSerializable(typeof(CharacterStateVersion[]))]
[JsonSerializable(typeof(CharacterSummary))]
[JsonSerializable(typeof(CharacterSummary[]))]
[JsonSerializable(typeof(CreateCampaignRequest))]
[JsonSerializable(typeof(CreateCharacterRequest))]
[JsonSerializable(typeof(CreateSkillGroupRequest))]
[JsonSerializable(typeof(CreateSkillRequest))]
[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(IReadOnlyList<AdminUserSummary>))]
[JsonSerializable(typeof(IReadOnlyList<CharacterStateVersion>))]
[JsonSerializable(typeof(IReadOnlyList<RollDieResult>))]
[JsonSerializable(typeof(IReadOnlyList<string>))]
[JsonSerializable(typeof(LoginRequest))]
[JsonSerializable(typeof(MeResponse))]
[JsonSerializable(typeof(RegisterRequest))]
[JsonSerializable(typeof(RollDieResult))]
[JsonSerializable(typeof(RollDieResult[]))]
[JsonSerializable(typeof(RollResult))]
[JsonSerializable(typeof(RollSkillRequest))]
[JsonSerializable(typeof(RulesetDefinition))]
[JsonSerializable(typeof(RulesetDefinition[]))]
[JsonSerializable(typeof(SkillGroupSummary))]
[JsonSerializable(typeof(SkillSummary))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(UpdateCharacterRequest))]
[JsonSerializable(typeof(UpdateSkillGroupRequest))]
[JsonSerializable(typeof(UpdateSkillRequest))]
[JsonSerializable(typeof(UpdateUserRolesRequest))]
[JsonSerializable(typeof(UserSummary))]
public partial class RpgRollerJsonSerializerContext : JsonSerializerContext
{
}

View File

@@ -1,10 +1,18 @@
using Microsoft.AspNetCore.ResponseCompression;
using RpgRoller.Api;
using RpgRoller.Components;
using RpgRoller.Contracts;
using RpgRoller.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
});
builder.Services.ConfigureHttpJsonOptions(options => RpgRollerJson.Configure(options.SerializerOptions));
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<RpgRollerApiClient>();
builder.Services.AddScoped<WorkspaceSessionTokenAccessor>();
@@ -25,6 +33,7 @@ if (!string.IsNullOrWhiteSpace(configuredPathBase))
app.UsePathBase(normalizedPathBase);
}
app.UseResponseCompression();
app.UseStaticFiles();
app.UseAntiforgery();

View File

@@ -797,7 +797,7 @@ public sealed class GameService : IGameService
var (user, campaign) = context.Value!;
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign)
.TakeLast(CampaignLogPageSize)
.TakeLast(CampaignLogHistoryWindowSize)
.Select(ToLogEntry)
.ToArray();
@@ -1596,9 +1596,9 @@ public sealed class GameService : IGameService
private static int NormalizeCampaignLogPageSize(int? limit)
{
if (!limit.HasValue)
return CampaignLogPageSize;
return CampaignLogLivePageSize;
return Math.Clamp(limit.Value, 1, CampaignLogPageSize);
return Math.Clamp(limit.Value, 1, CampaignLogLivePageSize);
}
private static UserAccount CloneUser(UserAccount user)
@@ -1692,8 +1692,9 @@ public sealed class GameService : IGameService
};
}
private const int CampaignLogPageSize = 100;
private static readonly JsonSerializerOptions DiceJsonOptions = new(JsonSerializerDefaults.Web);
private const int CampaignLogHistoryWindowSize = 100;
private const int CampaignLogLivePageSize = 25;
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
private readonly Dictionary<Guid, CampaignStateTracker> m_CampaignStateById = [];
private readonly Dictionary<Guid, Character> m_CharactersById = [];