From 17700e7f46863d15cad4a5e981203d4414e695a5 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:59:59 -0500 Subject: [PATCH 01/25] Move types into ".Species" Move ListSpeciesRequest, ListSpeciesResponse and ListSpeciesRequestHandler to be inside "species" namespaces. --- .../{ => Species}/ListSpeciesRequestHandler.cs | 2 +- PMDDesktop.Tests/Utils/TypeUtilsTests.cs | 1 + PMDDesktop/Requests/ListSpeciesRequest.cs | 7 ------- PMDDesktop/Requests/Species/ListSpeciesRequest.cs | 7 +++++++ PMDDesktop/Requests/{ => Species}/ListSpeciesResponse.cs | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) rename PMDDesktop.Server/RequestHandlers/{ => Species}/ListSpeciesRequestHandler.cs (93%) delete mode 100644 PMDDesktop/Requests/ListSpeciesRequest.cs create mode 100644 PMDDesktop/Requests/Species/ListSpeciesRequest.cs rename PMDDesktop/Requests/{ => Species}/ListSpeciesResponse.cs (72%) diff --git a/PMDDesktop.Server/RequestHandlers/ListSpeciesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs similarity index 93% rename from PMDDesktop.Server/RequestHandlers/ListSpeciesRequestHandler.cs rename to PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs index 70ce47a..fb5c522 100644 --- a/PMDDesktop.Server/RequestHandlers/ListSpeciesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs @@ -1,4 +1,4 @@ -using PMDDesktop.Requests; +using PMDDesktop.Requests.Species; using PMDDesktop.Server.Assets.Data; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; diff --git a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs index f99eda7..b9f784f 100644 --- a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs +++ b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs @@ -1,4 +1,5 @@ using PMDDesktop.Requests; +using PMDDesktop.Requests.Species; using PMDDesktop.Structs; using PMDDesktop.Utils; using System.Reflection; diff --git a/PMDDesktop/Requests/ListSpeciesRequest.cs b/PMDDesktop/Requests/ListSpeciesRequest.cs deleted file mode 100644 index 40096b3..0000000 --- a/PMDDesktop/Requests/ListSpeciesRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PMDDesktop.Requests; - -[ServerRequest("/api/list/species")] -public class ListSpeciesRequest : ServerRequest -{ - -} diff --git a/PMDDesktop/Requests/Species/ListSpeciesRequest.cs b/PMDDesktop/Requests/Species/ListSpeciesRequest.cs new file mode 100644 index 0000000..ff8353f --- /dev/null +++ b/PMDDesktop/Requests/Species/ListSpeciesRequest.cs @@ -0,0 +1,7 @@ +namespace PMDDesktop.Requests.Species; + +[ServerRequest("/api/species/list")] +public class ListSpeciesRequest : ServerRequest +{ + +} diff --git a/PMDDesktop/Requests/ListSpeciesResponse.cs b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs similarity index 72% rename from PMDDesktop/Requests/ListSpeciesResponse.cs rename to PMDDesktop/Requests/Species/ListSpeciesResponse.cs index a38970b..b3efae9 100644 --- a/PMDDesktop/Requests/ListSpeciesResponse.cs +++ b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs @@ -1,4 +1,4 @@ -namespace PMDDesktop.Requests; +namespace PMDDesktop.Requests.Species; public class ListSpeciesResponse : ServerResponse { From 83985556298dc6eabd521c12c165a8ccc63024b6 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:21:20 -0500 Subject: [PATCH 02/25] Move and rename types into ".PokemonTypes" PMDDesktop.Requests.ListTypesRequest -> PMDDesktop.Requests.PokemonTypes.ListPokemonTypesRequest PMDDesktop.Requests.ListTypesResponse -> PMDDesktop.Requests.PokemonTypes.ListPokemonTypesResponse PMDDesktop.Server.RequestHandlers.ListTypesRequestHandler -> PMDDesktop.Server.RequestHandlers.ListPokemonTypesRequestHandler --- .../ListPokemonTypesRequestHandler.cs} | 10 +++++----- PMDDesktop.Tests/Utils/TypeUtilsTests.cs | 7 ++++--- PMDDesktop/Requests/ListTypesRequest.cs | 7 ------- .../Requests/PokemonTypes/ListPokemonTypesRequest.cs | 7 +++++++ .../ListPokemonTypesResponse.cs} | 4 ++-- 5 files changed, 18 insertions(+), 17 deletions(-) rename PMDDesktop.Server/RequestHandlers/{ListTypesRequestHandler.cs => PokemonTypes/ListPokemonTypesRequestHandler.cs} (55%) delete mode 100644 PMDDesktop/Requests/ListTypesRequest.cs create mode 100644 PMDDesktop/Requests/PokemonTypes/ListPokemonTypesRequest.cs rename PMDDesktop/Requests/{ListTypesResponse.cs => PokemonTypes/ListPokemonTypesResponse.cs} (74%) diff --git a/PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs similarity index 55% rename from PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs rename to PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs index 56092e4..ebacdd8 100644 --- a/PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs @@ -1,14 +1,14 @@ -using PMDDesktop.Requests; +using PMDDesktop.Requests.PokemonTypes; using PMDDesktop.Server.Assets.Data; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; -namespace PMDDesktop.Server.RequestHandlers; +namespace PMDDesktop.Server.RequestHandlers.PokemonTypes; -public class ListTypesRequestHandler : JsonRequestHandler +public class ListPokemonTypesRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListTypesRequest request, GameServer game, User? requestingUser) + protected override async Task CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser) { return new() @@ -18,7 +18,7 @@ protected override async Task CreateResponse(ListTypesRequest } - private static ListTypesResponse.TypeEntry TypeAssetToTypeEntry(PokemonType type) + private static ListPokemonTypesResponse.TypeEntry TypeAssetToTypeEntry(PokemonType type) { return new() diff --git a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs index b9f784f..5504c45 100644 --- a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs +++ b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs @@ -1,4 +1,5 @@ using PMDDesktop.Requests; +using PMDDesktop.Requests.PokemonTypes; using PMDDesktop.Requests.Species; using PMDDesktop.Structs; using PMDDesktop.Utils; @@ -79,11 +80,11 @@ public static void GetInstanceableClassesTests(Type containsType, bool shouldCon // ListSpeciesRequest is a non-abstract class, so it SHOULD contain itself. [InlineData(typeof(ListSpeciesRequest), typeof(ListSpeciesRequest), true)] // ListTypesRequest is a non-abstract class that implements ServerRequest<>. - [InlineData(typeof(ServerRequest<>), typeof(ListTypesRequest), true)] + [InlineData(typeof(ServerRequest<>), typeof(ListPokemonTypesRequest), true)] // The function should pick up assignable generic functions. - [InlineData(typeof(ServerRequest), typeof(ListTypesRequest), true)] + [InlineData(typeof(ServerRequest), typeof(ListPokemonTypesRequest), true)] // It shouldn't get confused with non-assignable types that share the generic type. - [InlineData(typeof(ServerRequest), typeof(ListSpeciesRequest), false)] + [InlineData(typeof(ServerRequest), typeof(ListSpeciesRequest), false)] public static void GetInstanceableClassesAssignableToTests(Type startingType, Type containsType, bool shouldContain) { diff --git a/PMDDesktop/Requests/ListTypesRequest.cs b/PMDDesktop/Requests/ListTypesRequest.cs deleted file mode 100644 index db5129a..0000000 --- a/PMDDesktop/Requests/ListTypesRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PMDDesktop.Requests; - -[ServerRequest("/api/list/types")] -public class ListTypesRequest : ServerRequest -{ - -} diff --git a/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesRequest.cs b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesRequest.cs new file mode 100644 index 0000000..01c54b1 --- /dev/null +++ b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesRequest.cs @@ -0,0 +1,7 @@ +namespace PMDDesktop.Requests.PokemonTypes; + +[ServerRequest("/api/pokemon-types/list")] +public class ListPokemonTypesRequest : ServerRequest +{ + +} diff --git a/PMDDesktop/Requests/ListTypesResponse.cs b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs similarity index 74% rename from PMDDesktop/Requests/ListTypesResponse.cs rename to PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs index c1a85e2..66b7dac 100644 --- a/PMDDesktop/Requests/ListTypesResponse.cs +++ b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs @@ -1,6 +1,6 @@ -namespace PMDDesktop.Requests; +namespace PMDDesktop.Requests.PokemonTypes; -public class ListTypesResponse : ServerResponse +public class ListPokemonTypesResponse : ServerResponse { public class TypeEntry From b0c25ed0c691b2b8b9ad08bb2d0216a1a0d695c3 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:25:18 -0500 Subject: [PATCH 03/25] Rename ServerResponse to ServerJSONResponse WIP feature!! The code probably doesn't make a whole lot of sense right now! --- PMDDesktop.Server/RequestHandlers/IRequestHandler.cs | 4 ++-- PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs | 2 +- PMDDesktop/Requests/Auth/LoginResponse.cs | 2 +- PMDDesktop/Requests/Auth/WhoAmIResponse.cs | 2 +- .../Requests/Permissions/ListPermissionsResponse.cs | 2 +- .../Requests/Permissions/PermissionInfoResponse.cs | 2 +- .../Requests/PokemonTypes/ListPokemonTypesResponse.cs | 2 +- PMDDesktop/Requests/Roles/ListRolesResponse.cs | 2 +- PMDDesktop/Requests/Roles/RoleInfoResponse.cs | 2 +- PMDDesktop/Requests/ServerImageResponse.cs | 2 +- PMDDesktop/Requests/ServerJsonResponse.cs | 9 +++++++++ PMDDesktop/Requests/ServerResponse.cs | 1 - PMDDesktop/Requests/Species/ListSpeciesResponse.cs | 2 +- PMDDesktop/Requests/Users/ListUsersResponse.cs | 2 +- PMDDesktop/Requests/Users/UserInfoResponse.cs | 2 +- 15 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 PMDDesktop/Requests/ServerJsonResponse.cs diff --git a/PMDDesktop.Server/RequestHandlers/IRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/IRequestHandler.cs index d715491..d3039f5 100644 --- a/PMDDesktop.Server/RequestHandlers/IRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/IRequestHandler.cs @@ -26,10 +26,10 @@ static void AddApiPoints(GameServer server) Type requestType = handler.GetRequestType(); - if (!ServerRequest.ATTRIBUTES.TryGetValue(requestType, out ServerRequestAttribute? attribute)) + if (!ServerRequest.ATTRIBUTES.TryGetValue(requestType, out ServerRequestAttribute? attribute)) throw new MissingAttributeException(requestType, typeof(ServerRequestAttribute)); - EndpointMapper mapper = ServerRequest.IsAllQueryProperties(requestType) + EndpointMapper mapper = ServerRequest.IsAllQueryProperties(requestType) ? server.WebApp.MapGet : server.WebApp.MapPost; diff --git a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs index 9dfdd1a..0f2b70f 100644 --- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs @@ -9,7 +9,7 @@ namespace PMDDesktop.Server.RequestHandlers; -public abstract class JsonRequestHandler : IRequestHandler where TReq : ServerRequest where TRes : ServerResponse +public abstract class JsonRequestHandler : IRequestHandler where TReq : ServerRequest where TRes : ServerJsonResponse { private MemberInfo[] queryParameters; diff --git a/PMDDesktop/Requests/Auth/LoginResponse.cs b/PMDDesktop/Requests/Auth/LoginResponse.cs index 81e8c17..57767a9 100644 --- a/PMDDesktop/Requests/Auth/LoginResponse.cs +++ b/PMDDesktop/Requests/Auth/LoginResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Auth; -public sealed class LoginResponse : ServerResponse +public sealed class LoginResponse : ServerJsonResponse { public required string AuthorizationToken { get; set; } diff --git a/PMDDesktop/Requests/Auth/WhoAmIResponse.cs b/PMDDesktop/Requests/Auth/WhoAmIResponse.cs index afa4623..6a9a023 100644 --- a/PMDDesktop/Requests/Auth/WhoAmIResponse.cs +++ b/PMDDesktop/Requests/Auth/WhoAmIResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Auth; -public sealed class WhoAmIResponse : ServerResponse +public sealed class WhoAmIResponse : ServerJsonResponse { public required Guid GUID {get; set;} diff --git a/PMDDesktop/Requests/Permissions/ListPermissionsResponse.cs b/PMDDesktop/Requests/Permissions/ListPermissionsResponse.cs index c88d2a8..08d700d 100644 --- a/PMDDesktop/Requests/Permissions/ListPermissionsResponse.cs +++ b/PMDDesktop/Requests/Permissions/ListPermissionsResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Permissions; -public sealed class ListPermissionsResponse : ServerResponse +public sealed class ListPermissionsResponse : ServerJsonResponse { /// diff --git a/PMDDesktop/Requests/Permissions/PermissionInfoResponse.cs b/PMDDesktop/Requests/Permissions/PermissionInfoResponse.cs index eafbdae..cd3a28a 100644 --- a/PMDDesktop/Requests/Permissions/PermissionInfoResponse.cs +++ b/PMDDesktop/Requests/Permissions/PermissionInfoResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Permissions; -public sealed class PermissionInfoResponse : ServerResponse +public sealed class PermissionInfoResponse : ServerJsonResponse { /// diff --git a/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs index 66b7dac..1e08890 100644 --- a/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs +++ b/PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.PokemonTypes; -public class ListPokemonTypesResponse : ServerResponse +public class ListPokemonTypesResponse : ServerJsonResponse { public class TypeEntry diff --git a/PMDDesktop/Requests/Roles/ListRolesResponse.cs b/PMDDesktop/Requests/Roles/ListRolesResponse.cs index 89f7247..6a46adc 100644 --- a/PMDDesktop/Requests/Roles/ListRolesResponse.cs +++ b/PMDDesktop/Requests/Roles/ListRolesResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Roles; -public sealed class ListRolesResponse : ServerResponse +public sealed class ListRolesResponse : ServerJsonResponse { /// diff --git a/PMDDesktop/Requests/Roles/RoleInfoResponse.cs b/PMDDesktop/Requests/Roles/RoleInfoResponse.cs index 205436b..5129063 100644 --- a/PMDDesktop/Requests/Roles/RoleInfoResponse.cs +++ b/PMDDesktop/Requests/Roles/RoleInfoResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Roles; -public sealed class RoleInfoResponse : ServerResponse +public sealed class RoleInfoResponse : ServerJsonResponse { /// diff --git a/PMDDesktop/Requests/ServerImageResponse.cs b/PMDDesktop/Requests/ServerImageResponse.cs index a025718..5741628 100644 --- a/PMDDesktop/Requests/ServerImageResponse.cs +++ b/PMDDesktop/Requests/ServerImageResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests; -public class ServerImageResponse : ServerResponse +public class ServerImageResponse : ServerJsonResponse { diff --git a/PMDDesktop/Requests/ServerJsonResponse.cs b/PMDDesktop/Requests/ServerJsonResponse.cs new file mode 100644 index 0000000..c4edde4 --- /dev/null +++ b/PMDDesktop/Requests/ServerJsonResponse.cs @@ -0,0 +1,9 @@ +namespace PMDDesktop.Requests; + +public abstract class ServerJsonResponse : ServerResponse +{ + + + +} + diff --git a/PMDDesktop/Requests/ServerResponse.cs b/PMDDesktop/Requests/ServerResponse.cs index 51502d2..8dddd9a 100644 --- a/PMDDesktop/Requests/ServerResponse.cs +++ b/PMDDesktop/Requests/ServerResponse.cs @@ -6,4 +6,3 @@ public abstract class ServerResponse } - diff --git a/PMDDesktop/Requests/Species/ListSpeciesResponse.cs b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs index b3efae9..3173342 100644 --- a/PMDDesktop/Requests/Species/ListSpeciesResponse.cs +++ b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Species; -public class ListSpeciesResponse : ServerResponse +public class ListSpeciesResponse : ServerJsonResponse { public required string[] SpeciesIDs { get; init; } diff --git a/PMDDesktop/Requests/Users/ListUsersResponse.cs b/PMDDesktop/Requests/Users/ListUsersResponse.cs index fa84038..fc90898 100644 --- a/PMDDesktop/Requests/Users/ListUsersResponse.cs +++ b/PMDDesktop/Requests/Users/ListUsersResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Users; -public sealed class ListUsersResponse : ServerResponse +public sealed class ListUsersResponse : ServerJsonResponse { /// diff --git a/PMDDesktop/Requests/Users/UserInfoResponse.cs b/PMDDesktop/Requests/Users/UserInfoResponse.cs index a143260..759b1ba 100644 --- a/PMDDesktop/Requests/Users/UserInfoResponse.cs +++ b/PMDDesktop/Requests/Users/UserInfoResponse.cs @@ -1,6 +1,6 @@ namespace PMDDesktop.Requests.Users; -public sealed class UserInfoResponse : ServerResponse +public sealed class UserInfoResponse : ServerJsonResponse { public required string DisplayName { get; set; } From eb3133930f1e1a387105d8cc1973614124c6401f Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:12:28 -0500 Subject: [PATCH 04/25] Add result-pattern --- PMDDesktop/Results/Result.cs | 48 ++++++++++++++++++++++ PMDDesktop/Results/ResultError.cs | 8 ++++ PMDDesktop/Results/ResultExceptionError.cs | 10 +++++ 3 files changed, 66 insertions(+) create mode 100644 PMDDesktop/Results/Result.cs create mode 100644 PMDDesktop/Results/ResultError.cs create mode 100644 PMDDesktop/Results/ResultExceptionError.cs diff --git a/PMDDesktop/Results/Result.cs b/PMDDesktop/Results/Result.cs new file mode 100644 index 0000000..820b1c7 --- /dev/null +++ b/PMDDesktop/Results/Result.cs @@ -0,0 +1,48 @@ + +using System.Diagnostics.CodeAnalysis; + +namespace PMDDesktop.Results; + +/// +/// Very generic "result-pattern" class. +/// Use this for any function that may error but where try-catch can be avoided. +/// Use to get the result/error in a "try-pattern." +/// +/// +/// +public class Result where TError : ResultError +{ + + private TResult? result; + private TError? error; + + public Result(TError error) + { + this.error = error; + } + + public Result(TResult result) + { + this.result = result; + } + + public bool HasResult() => error is not null; + public bool HasError() => result is not null; + + /// + /// + /// + /// + /// + /// + public bool TryResult([NotNullWhen(true)] out TResult? result, [NotNullWhen(false)] out TError? error) + { + + result = this.result; + error = this.error; + + return result is not null; + + } + +} diff --git a/PMDDesktop/Results/ResultError.cs b/PMDDesktop/Results/ResultError.cs new file mode 100644 index 0000000..3d99c0b --- /dev/null +++ b/PMDDesktop/Results/ResultError.cs @@ -0,0 +1,8 @@ +namespace PMDDesktop.Results; + +public abstract class ResultError +{ + + public abstract string GetErrorReason(); + +} diff --git a/PMDDesktop/Results/ResultExceptionError.cs b/PMDDesktop/Results/ResultExceptionError.cs new file mode 100644 index 0000000..4376108 --- /dev/null +++ b/PMDDesktop/Results/ResultExceptionError.cs @@ -0,0 +1,10 @@ +namespace PMDDesktop.Results; + +public class ResultExceptionError(Exception exception) : ResultError +{ + + public Exception Exception { get; private init; } = exception; + + public override string GetErrorReason() => $"Exception occurred: '{Exception.Message}'"; + +} From ecab3219b232d519f94a61ab1672b959b50d5705 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:37:32 -0500 Subject: [PATCH 05/25] Use Result-Pattern in RequestHandlers (breaking) --- .../GenericResponseCreators.cs | 10 +- .../RequestHandlers/JsonRequestHandler.cs | 94 ++++++------------- .../RequestHandlers/ResponseJsonResult.cs | 11 +++ .../RequestHandlers/ResponseResult.cs | 11 +++ .../ResponseResultAuthenticationError.cs | 18 ++++ .../RequestHandlers/ResponseResultError.cs | 10 ++ .../ResponseResultExceptionError.cs | 10 ++ .../ResponseResultPermissionError.cs | 22 +++++ .../RequestHandlers/UserRequestException.cs | 8 -- 9 files changed, 118 insertions(+), 76 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResult.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs delete mode 100644 PMDDesktop.Server/RequestHandlers/UserRequestException.cs diff --git a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs index d7f712f..93f8088 100644 --- a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs +++ b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs @@ -1,4 +1,5 @@ -using PMDDesktop.Requests.Permissions; +using System.Diagnostics.CodeAnalysis; +using PMDDesktop.Requests.Permissions; using PMDDesktop.Requests.Roles; using PMDDesktop.Requests.Users; using PMDDesktop.Server.Users; @@ -9,6 +10,13 @@ namespace PMDDesktop.Server.RequestHandlers; internal static class GenericResponseCreators { + internal static bool TryAuthenticateUser([NotNullWhen(true)] User? user, UserPermission[] permissionsNeeded, [NotNullWhen(false)] out ResponseResultError? error) + { + + + + } + internal static UserInfoResponse CreateUserInfoResponse(User fromPOV, User ofUser) { diff --git a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs index 0f2b70f..24890f3 100644 --- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs @@ -1,7 +1,9 @@ -using PMDDesktop.Requests; +using Microsoft.AspNetCore.Http.HttpResults; +using PMDDesktop.Requests; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; using PMDDesktop.Utils; +using System.Buffers; using System.Reflection; using System.Text; using System.Text.Json; @@ -52,11 +54,11 @@ private async Task GetJsonObject(HttpContext context) try { json = (JsonObject?)await JsonNode.ParseAsync(context.Request.Body) - ?? throw new UserRequestException("Parsed JsonNode was not a JsonObject or otherwise null."); + ?? throw new Exception("Parsed JsonNode was not a JsonObject or otherwise null."); // TODO: Find a way to do this without throwing } catch (JsonException e) { - throw new UserRequestException($"Error occurred while parsing JSON: {e.Message}", e); + throw new Exception($"Error occurred while parsing JSON: {e.Message}", e); // TODO: Find a way to do this without throwing } // Other exceptions should be unhandled. They'll be caught by HandleRequest as a 500 error. @@ -67,7 +69,7 @@ private async Task GetJsonObject(HttpContext context) { if (json.ContainsKey(member.Name)) - throw new UserRequestException($"'{member}' should be passed as a query parameter (url components after the ?) and not inside the JSON object."); + throw new Exception($"'{member}' should be passed as a query parameter (url components after the ?) and not inside the JSON object."); // TODO: Find a way to do this without throwing } @@ -81,12 +83,12 @@ private async Task GetJsonObject(HttpContext context) // Get the member we're looking at, and remove it from our query list. MemberInfo member = acceptableQueries.Find((member) => member.Name == key) - ?? throw new UserRequestException($"Wasn't expecting a query with key '{key}'"); + ?? throw new Exception($"Wasn't expecting a query with key '{key}'"); // TODO: Find a way to do this without throwing acceptableQueries.Remove(member); if (query.Value.Count != 1) // We only allow one of each query. { - throw new UserRequestException($"Cannot handle {query.Value.Count} values for {key}"); + throw new Exception($"Cannot handle {query.Value.Count} values for {key}"); // TODO: Find a way to do this without throwing } // Get that value, now that we know it's a singleton, and parse it into a node. @@ -101,7 +103,7 @@ private async Task GetJsonObject(HttpContext context) } catch (JsonException e) { - throw new UserRequestException($"Error occurred while parsing query '{key}' as JSON: {e.Message}", e); + throw new Exception($"Error occurred while parsing query '{key}' as JSON: {e.Message}", e); // TODO: Find a way to do this without throwing } json.Add(key, node); @@ -121,90 +123,48 @@ public async Task HandleRequest(HttpContext context, GameServer game) JsonObject json; + ResponseJsonResult result; + try { json = await GetJsonObject(context); - } - catch (UserRequestException e) // Intended for user-facing errors, like bad inputs. - { - - await WritePlainText(context, 400, e.Message); - return; - - } - catch (Exception e) - { - - await WritePlainText(context, 500, "Internal error while creating json object: " + e.Message); - return; + TReq deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS) + ?? throw new Exception($"Deserialized data from {context} was null."); - } + if (IRequestHandler.TryGetUserFromContext(game.State.Users, context, out User? user, out UserAccessToken? token)) + if (token.NeedsRefresh()) + await token.RefreshToken(); - TReq deserialized; - - try - { - - deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS) - ?? throw new Exception($"Deserialized data from {context} was null."); + result = await CreateResponse(deserialized, game, user); } catch (Exception e) { - await WritePlainText(context, 400, "Error while deserializing JSON: " + e.Message); - return; - - } - - if (IRequestHandler.TryGetUserFromContext(game.State.Users, context, out User? user, out UserAccessToken? token)) - if (token.NeedsRefresh()) - await token.RefreshToken(); - - try - { - - context.Response.StatusCode = 200; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsJsonAsync(await CreateResponse(deserialized, game, user), AppInfo.NETWORK_JSON_OPTIONS); + result = new(new ResponseResultExceptionError(e)); } - catch (UserRequestException e) // Intended for user-facing errors, like bad inputs, - { - - await WritePlainText(context, 400, e.Message); - return; - } - catch (Exception e) + if (!result.TryResult(out TRes? success, out ResponseResultError? error)) { - - await WritePlainText(context, 500, "Internal error while processing request: " + e.Message); + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = error.GetReturnCode(); + byte[] messageBytes = Encoding.UTF8.GetBytes(error.GetErrorReason()); + await context.Response.BodyWriter.WriteAsync(messageBytes); return; - } - } + context.Response.ContentType = "application/json"; + context.Response.StatusCode = 200; - /// - /// Helper function to write a plain text response. Intended for error messages. - /// - /// Context to write to. - /// Code to return. - /// Message to send as a response. - /// - private static async Task WritePlainText(HttpContext context, int code, string errorMessage) - { + await context.Response.WriteAsJsonAsync(success, AppInfo.NETWORK_JSON_OPTIONS); - context.Response.StatusCode = code; - context.Response.ContentType = "text/plain"; - await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(errorMessage)); return; } - protected abstract Task CreateResponse(TReq request, GameServer game, User? requestingUser); + protected abstract Task> CreateResponse(TReq request, GameServer game, User? requestingUser); } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs new file mode 100644 index 0000000..c9c9c39 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs @@ -0,0 +1,11 @@ +using PMDDesktop.Requests; + +namespace PMDDesktop.Server.RequestHandlers; + +public sealed class ResponseJsonResult : ResponseResult where TJsonResponse : ServerJsonResponse where TResponseError : ResponseResultError +{ + + public ResponseJsonResult(TResponseError error) : base(error) { } + public ResponseJsonResult(TJsonResponse result) : base(result) { } + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs new file mode 100644 index 0000000..701025a --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs @@ -0,0 +1,11 @@ +using PMDDesktop.Results; + +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResult : Result where TError : ResponseResultError +{ + + public ResponseResult(TError error) : base(error) { } + public ResponseResult(TResult result) : base(result) { } + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs new file mode 100644 index 0000000..cb66db3 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs @@ -0,0 +1,18 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultAuthenticationError(string? providedTokenString) : ResponseResultError +{ + + public string? ProvidedTokenString { get; private init; } = providedTokenString; + public override string GetErrorReason() + { + + return ProvidedTokenString is not null + ? $"The provided token string does not authenticate you. Your login may have expired, or it may be formatted incorrectly." + : $"You need to be authenicated to access this endpoint. Please provide a valid token in the 'Authorization' header."; + + } + + public override int GetReturnCode() => 401; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs new file mode 100644 index 0000000..6a2b38e --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs @@ -0,0 +1,10 @@ +using PMDDesktop.Results; + +namespace PMDDesktop.Server.RequestHandlers; + +public abstract class ResponseResultError : ResultError +{ + + public abstract int GetReturnCode(); + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs new file mode 100644 index 0000000..546d52f --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs @@ -0,0 +1,10 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public sealed class ResponseResultExceptionError(Exception exception) : ResponseResultError +{ + + public Exception Exception { get; private init; } = exception; + public override string GetErrorReason() => $"Server-side exception of type '{Exception.GetType().Name}' with message: {Exception.Message}"; + public override int GetReturnCode() => 500; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs new file mode 100644 index 0000000..bb684fe --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs @@ -0,0 +1,22 @@ +using PMDDesktop.Server.Users.Roles; + +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultPermissionError(params UserPermission[] permissions) : ResponseResultError +{ + + public UserPermission[] Permissions { get; private init; } = permissions; + public override string GetErrorReason() + { + + string permList = string.Join(", ", Permissions); + + return Permissions.Length == 1 + ? $"You do not have a required permission: {permList}" + : $"You do not have the required permissions: {permList}"; + + } + + public override int GetReturnCode() => 403; + +} diff --git a/PMDDesktop.Server/RequestHandlers/UserRequestException.cs b/PMDDesktop.Server/RequestHandlers/UserRequestException.cs deleted file mode 100644 index 3b924cf..0000000 --- a/PMDDesktop.Server/RequestHandlers/UserRequestException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PMDDesktop.Server.RequestHandlers; - -internal class UserRequestException(string message, Exception? innerException = null) : Exception(message, innerException) -{ - - - -} From edcaea4b25e8f0d32e2b701209b7224a7146fb81 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:38:12 -0500 Subject: [PATCH 06/25] Use Result-Pattern in Auth RequestHandlers. --- .../RequestHandlers/Auth/LoginRequestHandler.cs | 10 +++++----- .../RequestHandlers/Auth/ResponseResultLoginError.cs | 11 +++++++++++ .../RequestHandlers/Auth/WhoAmIRequestHandler.cs | 10 +++++----- 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs diff --git a/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs index 0d3c6d4..bb412e4 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs @@ -7,19 +7,19 @@ namespace PMDDesktop.Server.RequestHandlers.Auth; public class LoginRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(LoginRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(LoginRequest request, GameServer game, User? requestingUser) { if (!game.State.Users.TryGetUserByLoginHandle(request.Handle, out User? user)) - throw new UserRequestException($"Invalid username or password."); + return new(new ResponseResultLoginError()); if (!await user.VerifyPassword(request.Password)) - throw new UserRequestException($"Invalid username or password."); // Throw the same thing to be more "secure" I guess. + return new(new ResponseResultLoginError()); - return new() + return new(new LoginResponse() { AuthorizationToken = (await user.CreateAccessToken()).TokenString - }; + }); } diff --git a/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs b/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs new file mode 100644 index 0000000..ac3cdb5 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs @@ -0,0 +1,11 @@ +using PMDDesktop.Server.RequestHandlers; + +namespace PMDDesktop.Server.RequestHandlers.Auth; + +public sealed class ResponseResultLoginError() : ResponseResultError +{ + + public override string GetErrorReason() => $"Invalid handle or password."; + public override int GetReturnCode() => 400; + +} diff --git a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs index 5457b15..15f6cb8 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs @@ -7,16 +7,16 @@ namespace PMDDesktop.Server.RequestHandlers.Auth; public class WhoAmIRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"Please send your valid token in the Authorization header to access this endpoint."); + if (!GenericResponseCreators.TryAuthenticateUser(requestingUser, [], out ResponseResultError? error)) + return new(error); - return new() + return new(new WhoAmIResponse() { GUID = requestingUser.GUID - }; + }); } From bf592a5c1ab8a759fd093a698a96dd135ca57fe2 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:55:45 -0500 Subject: [PATCH 07/25] IEnumerable in ResponseResultPermissionError --- .../RequestHandlers/ResponseResultPermissionError.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs index bb684fe..f55cf46 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs @@ -2,10 +2,10 @@ namespace PMDDesktop.Server.RequestHandlers; -public class ResponseResultPermissionError(params UserPermission[] permissions) : ResponseResultError +public class ResponseResultPermissionError(params IEnumerable permissions) : ResponseResultError { - public UserPermission[] Permissions { get; private init; } = permissions; + public UserPermission[] Permissions { get; private init; } = [.. permissions]; public override string GetErrorReason() { From e8be031d5942335d8bb14b257a209ce1ab0d1558 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:10:30 -0500 Subject: [PATCH 08/25] Add logic in TryAuthenticateUser --- .../RequestHandlers/GenericResponseCreators.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs index 93f8088..54dc7b8 100644 --- a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs +++ b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs @@ -13,7 +13,22 @@ internal static class GenericResponseCreators internal static bool TryAuthenticateUser([NotNullWhen(true)] User? user, UserPermission[] permissionsNeeded, [NotNullWhen(false)] out ResponseResultError? error) { + if (user is null) + { + error = new ResponseResultAuthenticationError(null); + return false; + } + + IEnumerable missingPermissions = permissionsNeeded.Where(permission => !user.HasPermission(permission)); + + if (!missingPermissions.Any()) + { + error = new ResponseResultPermissionError(missingPermissions); + return false; + } + error = null; + return true; } From 79e07d232f680b76eec1c05edbb9f9c951402d28 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:10:56 -0500 Subject: [PATCH 09/25] Use Result-Pattern in Permissions RequestHandlers --- .../Permissions/ListPermissionsRequestHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs index fafd792..13b0ffd 100644 --- a/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs @@ -8,13 +8,13 @@ namespace PMDDesktop.Server.RequestHandlers.Permissions; public class ListPermissionsRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListPermissionsRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListPermissionsRequest request, GameServer game, User? requestingUser) { - return new() + return new(new ListPermissionsResponse() { - Permissions = [.. UserPermission.ALL.Select(permission => GenericResponseCreators.CreatePermissionInfoResponse(permission))] - }; + Permissions = [.. UserPermission.ALL.Select(GenericResponseCreators.CreatePermissionInfoResponse)] + }); } From 26ebee99d77ee35fd709b23f63f9f220e85d27cf Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:11:55 -0500 Subject: [PATCH 10/25] Use Result-Pattern in PokemonTypes RequestHandlers --- .../PokemonTypes/ListPokemonTypesRequestHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs index ebacdd8..398cc43 100644 --- a/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs @@ -8,13 +8,13 @@ namespace PMDDesktop.Server.RequestHandlers.PokemonTypes; public class ListPokemonTypesRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser) { - return new() + return new(new ListPokemonTypesResponse() { Types = [.. game.State.Assets.OfType().Select(TypeAssetToTypeEntry)] - }; + }); } From dc98f64f2d7f5f7cb39d1c897f82dc7279b7773c Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:36:19 -0500 Subject: [PATCH 11/25] Add CommonResponseHandlerChecks This splits off of GenericReponseCreators, moving the function that used to live there. --- .../Auth/WhoAmIRequestHandler.cs | 2 +- .../CommonResponseHandlerChecks.cs | 114 ++++++++++++++++++ .../GenericResponseCreators.cs | 25 +--- .../ResponseResultDataMissingError.cs | 12 ++ 4 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs diff --git a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs index 15f6cb8..b996e33 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs @@ -10,7 +10,7 @@ public class WhoAmIRequestHandler : JsonRequestHandler> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser) { - if (!GenericResponseCreators.TryAuthenticateUser(requestingUser, [], out ResponseResultError? error)) + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error)) return new(error); return new(new WhoAmIResponse() diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs new file mode 100644 index 0000000..44b4ad0 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.CodeAnalysis; +using PMDDesktop.Requests.Roles; +using PMDDesktop.Requests.Users; +using PMDDesktop.Server.Assets; +using PMDDesktop.Server.Saving; +using PMDDesktop.Server.Users; +using PMDDesktop.Server.Users.Roles; + +namespace PMDDesktop.Server.RequestHandlers; + +internal static class CommonResponseHandlerChecks +{ + + /// + /// Try to authenticate a , and authorize them against certain permissions. + /// To pass, the needs to both be not null, and have all the s in . + /// + /// The to check the authentication and authorization of. + /// A list of all permissions needed to be fully authorized. + /// A or describing why authentication/authorization failed, to be returned. + /// True if authenticated and authorized; if false, contains the reason the authentication/authorization failed. + internal static bool TryAuthorizeUser([NotNullWhen(true)] User? user, IEnumerable permissionsNeeded, [NotNullWhen(false)] out ResponseResultError? error) + { + + if (user is null) + { + error = new ResponseResultAuthenticationError(null); + return false; + } + + IEnumerable missingPermissions = permissionsNeeded.Where(permission => !user.HasPermission(permission)); + + if (missingPermissions.Any()) + { + error = new ResponseResultPermissionError(missingPermissions); + return false; + } + + error = null; + return true; + + } + + internal static bool TryGetUser(IUserIndexable indexable, Guid guid, [NotNullWhen(true)] out User? user, [NotNullWhen(false)] out ResponseResultError? error) + { + + if (indexable.TryGetUser(guid, out user)) + { + error = null; + return true; + } + + error = new ResponseResultDataMissingError(guid.ToString(), typeof(User)); + return false; + + } + + internal static bool TryGetRole(IRoleIndexable indexable, Guid guid, [NotNullWhen(true)] out UserRole? role, [NotNullWhen(false)] out ResponseResultError? error) + { + + if (indexable.TryGetRole(guid, out role)) + { + error = null; + return true; + } + + error = new ResponseResultDataMissingError(guid.ToString(), typeof(UserRole)); + return false; + + } + + internal static bool TryGetPermission(string dataName, [NotNullWhen(true)] out UserPermission? permission, [NotNullWhen(false)] out ResponseResultError? error) + { + + if (UserPermission.TryGetPermissionByDataName(dataName, out permission)) + { + error = null; + return true; + } + + error = new ResponseResultDataMissingError(dataName, typeof(UserPermission)); + return false; + + } + + internal static bool TryGetSaveData(ISaveDataIndexable indexable, Guid guid, [NotNullWhen(true)] out TData? data, [NotNullWhen(false)] out ResponseResultError? error) where TData : SaveData + { + + if (indexable.TryGetSave(guid, out data)) + { + error = null; + return true; + } + + error = new ResponseResultDataMissingError(guid.ToString(), typeof(TData)); + return false; + + } + + internal static bool TryGetAsset(IAssetIndexable indexable, AssetLocation location, [NotNullWhen(true)] out TAsset? asset, [NotNullWhen(false)] out ResponseResultError? error) where TAsset : Asset + { + + if (indexable.TryGetAsset(location, out asset)) + { + error = null; + return true; + } + + error = new ResponseResultDataMissingError(location, typeof(TAsset)); + return false; + + } + +} diff --git a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs index 54dc7b8..d7f712f 100644 --- a/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs +++ b/PMDDesktop.Server/RequestHandlers/GenericResponseCreators.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using PMDDesktop.Requests.Permissions; +using PMDDesktop.Requests.Permissions; using PMDDesktop.Requests.Roles; using PMDDesktop.Requests.Users; using PMDDesktop.Server.Users; @@ -10,28 +9,6 @@ namespace PMDDesktop.Server.RequestHandlers; internal static class GenericResponseCreators { - internal static bool TryAuthenticateUser([NotNullWhen(true)] User? user, UserPermission[] permissionsNeeded, [NotNullWhen(false)] out ResponseResultError? error) - { - - if (user is null) - { - error = new ResponseResultAuthenticationError(null); - return false; - } - - IEnumerable missingPermissions = permissionsNeeded.Where(permission => !user.HasPermission(permission)); - - if (!missingPermissions.Any()) - { - error = new ResponseResultPermissionError(missingPermissions); - return false; - } - - error = null; - return true; - - } - internal static UserInfoResponse CreateUserInfoResponse(User fromPOV, User ofUser) { diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs new file mode 100644 index 0000000..631c5b0 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs @@ -0,0 +1,12 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultDataMissingError(string indexer, Type type) : ResponseResultError +{ + + public string Indexer { get; private init; } = indexer; + public Type Type { get; private init; } = type; + + public override string GetErrorReason() => $"Unable to find a {Type.Name} with '${Indexer}'."; + public override int GetReturnCode() => 400; + +} From 46793aaa6400a8cf2260e04889f39822ef48e368 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:04:55 -0500 Subject: [PATCH 12/25] Use Result-Pattern in Roles RequestHandlers --- .../CommonResponseHandlerChecks.cs | 14 +++++++++++ .../ResponseResultHierarchyError.cs | 11 +++++++++ .../Roles/CreateRoleRequestHandler.cs | 12 ++++------ .../Roles/GetRoleInfoRequestHandler.cs | 12 +++++----- ...tHandler.cs => ListRolesRequestHandler.cs} | 10 ++++---- .../Roles/SetRoleNameRequestHandler.cs | 19 +++++++-------- .../Roles/SetRolePermissionRequestHandler.cs | 23 ++++++++----------- 7 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs rename PMDDesktop.Server/RequestHandlers/Roles/{ListUsersRequestHandler.cs => ListRolesRequestHandler.cs} (52%) diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs index 44b4ad0..ffbbe04 100644 --- a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs +++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs @@ -111,4 +111,18 @@ internal static bool TryGetAsset(IAssetIndexable indexable, AssetLocatio } + internal static bool TryCompareHierarchy(User requestingUser, IUserHierarchyComparable expectedLower, [NotNullWhen(false)] out ResponseResultError? error) + { + + if (!requestingUser.HigherThan(expectedLower)) + { + error = new ResponseResultHierarchyError(); + return false; + } + + error = null; + return true; + + } + } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs new file mode 100644 index 0000000..347936d --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs @@ -0,0 +1,11 @@ +using PMDDesktop.Server.Users.Roles; + +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultHierarchyError() : ResponseResultError +{ + + public override string GetErrorReason() => $"You are not high enough in the role hierarchy to perform this operation."; + public override int GetReturnCode() => 403; + +} diff --git a/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs index 1bcab1f..43a85bf 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs @@ -8,15 +8,11 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class CreateRoleRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(CreateRoleRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(CreateRoleRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); - - // To create roles, you need the MANAGE_ROLES permission. - if (!requestingUser.HasPermission(UserPermission.MANAGE_ROLES)) - throw new UserRequestException($"You need permission to manage roles to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error)) + return new(error); UserRole role = await game.State.Roles.TryCreateRole() ?? throw new Exception($"Unable to create role."); @@ -24,7 +20,7 @@ protected override async Task CreateResponse(CreateRoleRequest if (request.NewName is not null) await role.SetName(request.NewName); - return GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role); + return new(GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role)); } diff --git a/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs index ac72557..fc03424 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs @@ -8,16 +8,16 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class GetRoleInfoRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(GetRoleInfoRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(GetRoleInfoRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - if (!game.TryGetRole(request.GUID, out UserRole? role)) - throw new UserRequestException($"{request.GUID} does not match a role."); + if (!CommonResponseHandlerChecks.TryGetRole(game, request.GUID, out UserRole? role, out ResponseResultError? error2)) + return new(error2); - return GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role); + return new(GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role)); } diff --git a/PMDDesktop.Server/RequestHandlers/Roles/ListUsersRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs similarity index 52% rename from PMDDesktop.Server/RequestHandlers/Roles/ListUsersRequestHandler.cs rename to PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs index 6d2de5a..5697e10 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/ListUsersRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs @@ -7,16 +7,16 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class ListRolesRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListRolesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListRolesRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error)) + return new(error); - return new() + return new(new ListRolesResponse() { Roles = [.. game.State.Roles.Select(role => GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role))] - }; + }); } diff --git a/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs index e6d72bd..a5b5b58 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs @@ -8,24 +8,21 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class SetRoleNameRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(SetRoleNameRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetRoleNameRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error1)) + return new(error1); - if (!requestingUser.HasPermission(UserPermission.MANAGE_ROLES)) - throw new UserRequestException($"You need permission to manage roles to access this endpoint."); + if (!CommonResponseHandlerChecks.TryGetRole(game, request.GUID, out UserRole? role, out ResponseResultError? error2)) + return new(error2); - if (!game.TryGetRole(request.GUID, out UserRole? role)) - throw new UserRequestException($"{request.GUID} does not match a role."); - - if (!requestingUser.HigherThan(role)) - throw new UserRequestException($"You are not high enough in the role hierarchy to perform this operation."); + if (!CommonResponseHandlerChecks.TryCompareHierarchy(requestingUser, role, out ResponseResultError? error3)) + return new(error3); await role.SetName(request.NewName); - return GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role); + return new(GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role)); } diff --git a/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs index da5412b..e2d2c28 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs @@ -8,27 +8,24 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class SetRolePermissionRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(SetRolePermissionRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetRolePermissionRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error1)) + return new(error1); - if (!requestingUser.HasPermission(UserPermission.MANAGE_ROLES)) - throw new UserRequestException($"You need permission to manage roles to access this endpoint."); + if (!CommonResponseHandlerChecks.TryGetRole(game, request.GUID, out UserRole? role, out ResponseResultError? error2)) + return new(error2); - if (!game.TryGetRole(request.GUID, out UserRole? role)) - throw new UserRequestException($"{request.GUID} does not match a role."); + if (!CommonResponseHandlerChecks.TryCompareHierarchy(requestingUser, role, out ResponseResultError? error3)) + return new(error3); - if (!requestingUser.HigherThan(role)) - throw new UserRequestException($"You are not high enough in the role hierarchy to perform this operation."); - - if (!UserPermission.TryGetPermissionByDataName(request.PermissionDataName, out UserPermission? permission)) - throw new UserRequestException($"Could not find a permission with the name '{request.PermissionDataName}' to set."); + if (!CommonResponseHandlerChecks.TryGetPermission(request.PermissionDataName, out UserPermission? permission, out ResponseResultError? error4)) + return new(error4); await role.SetPermission(permission, request.NewPermissionValue); - return GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role); + return new(GenericResponseCreators.CreateRoleInfoResponse(requestingUser, role)); } From f54c91cfc644b952cb1c7ed29cfb10af4b048502 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:23:29 -0500 Subject: [PATCH 13/25] Use Result-Pattern in Species RequestHandlers --- .../Species/ListSpeciesRequestHandler.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs index fb5c522..9042ced 100644 --- a/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs @@ -1,20 +1,19 @@ using PMDDesktop.Requests.Species; -using PMDDesktop.Server.Assets.Data; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; -namespace PMDDesktop.Server.RequestHandlers; +namespace PMDDesktop.Server.RequestHandlers.Species; public class ListSpeciesRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser) { - return new() + return new(new ListSpeciesResponse() { - SpeciesIDs = [.. game.State.Assets.OfType().Select((species) => (string)species.Location)] - }; + SpeciesIDs = [.. game.State.Assets.OfType().Select((species) => (string)species.Location)] + }); } From 358c2228959c75aa74b1ecc7b41ab386671483eb Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:53:11 -0500 Subject: [PATCH 14/25] Use Result-Pattern in Users RequesetHandlers (fixing) --- .../ResponseResultDataCollisionError.cs | 12 ++++++ .../ResponseResultDataInvalidError.cs | 12 ++++++ .../Users/CreateUserRequestHandler.cs | 17 +++----- .../Users/GetUserInfoRequestHandler.cs | 12 +++--- .../Users/ListUsersRequestHandler.cs | 10 ++--- .../Users/SetUserLoginHandleRequestHandler.cs | 40 +++++++++++-------- .../Users/SetUserNameRequestHandler.cs | 30 ++++++++------ .../Users/SetUserPasswordRequestHandler.cs | 30 ++++++++------ 8 files changed, 101 insertions(+), 62 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs new file mode 100644 index 0000000..f6d168c --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs @@ -0,0 +1,12 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultDataCollisionError(string identifier, string providedData) : ResponseResultError +{ + + public string DataIdentifier { get; private init; } = identifier; + public string? ProvidedData { get; private init; } = providedData; + + public override string GetErrorReason() => $"The provided value for ''{DataIdentifier}', '{ProvidedData}', cannot be used because it collides or duplicates with other data."; + public override int GetReturnCode() => 400; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs new file mode 100644 index 0000000..9b1582a --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs @@ -0,0 +1,12 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultDataInvalidError(string identifier, string? providedData) : ResponseResultError +{ + + public string DataIdentifier { get; private init; } = identifier; + public string? ProvidedData { get; private init; } = providedData; + + public override string GetErrorReason() => $"'{ProvidedData}' is not valid for '{DataIdentifier}'."; + public override int GetReturnCode() => 400; + +} diff --git a/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs index 7246c19..8df4628 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs @@ -8,25 +8,20 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class CreateUserRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(CreateUserRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(CreateUserRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); - - // To create users, you need the MANAGE_USERS permission. - if (!requestingUser.HasPermission(UserPermission.MANAGE_USERS)) - throw new UserRequestException($"You need permission to manage users to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_USERS], out ResponseResultError? error)) + return new(error); // TEMPORARY SOLUTION // TODO: Replace this with a more elegant solution. I think we should make a new class or struct to hold these types of super normalized strings. bool validHandle = request.NewLoginHandle is null || request.NewLoginHandle.All(letter => char.IsAsciiLetterLower(letter) || char.IsAsciiDigit(letter) || letter == '-'); - if (!validHandle) - throw new UserRequestException($"The handle is invalid. Please make sure you use lowercase letters, numbers, or hyphens."); + return new(new ResponseResultDataInvalidError(nameof(request.NewLoginHandle), request.NewLoginHandle)); if (request.NewLoginHandle is not null && game.State.Users.TryGetUserByLoginHandle(request.NewLoginHandle, out _)) - throw new UserRequestException($"Provided login handle '{request.NewLoginHandle}' is already in use."); + return new(new ResponseResultDataCollisionError(nameof(request.NewLoginHandle), request.NewLoginHandle)); UserCreationOptions options = new() { @@ -38,7 +33,7 @@ protected override async Task CreateResponse(CreateUserRequest User user = await game.State.Users.TryCreateUser(options) ?? throw new Exception($"Unable to create user."); - return GenericResponseCreators.CreateUserInfoResponse(requestingUser, user); + return new(GenericResponseCreators.CreateUserInfoResponse(requestingUser, user)); } diff --git a/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs index d164118..390ed9b 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs @@ -7,16 +7,16 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class GetUserInfoRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(GetUserInfoRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(GetUserInfoRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - if (!game.TryGetUser(request.GUID, out User? user)) - throw new UserRequestException($"{request.GUID} does not match a user."); + if (!CommonResponseHandlerChecks.TryGetUser(game, request.GUID, out User? user, out ResponseResultError? error2)) + return new(error2); - return GenericResponseCreators.CreateUserInfoResponse(requestingUser, user); + return new(GenericResponseCreators.CreateUserInfoResponse(requestingUser, user)); } diff --git a/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs index db728de..ac06e69 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs @@ -7,16 +7,16 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class ListUsersRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(ListUsersRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListUsersRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - return new() + return new(new ListUsersResponse() { Users = [.. game.State.Users.Select(user => GenericResponseCreators.CreateUserInfoResponse(requestingUser, user))] - }; + }); } diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs index 1767f9f..a9519a4 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs @@ -1,42 +1,50 @@ using PMDDesktop.Requests.Users; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; -using PMDDesktop.Server.Users.Roles; namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserLoginHandleRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(SetUserLoginHandleRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserLoginHandleRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + // Check that they're logged in. We don't know what permissions they need yet. + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - if (!game.TryGetUser(request.GUID, out User? user)) - throw new UserRequestException($"{request.GUID} does not match a user."); + if (!CommonResponseHandlerChecks.TryGetUser(game, request.GUID, out User? targetUser, out ResponseResultError? error2)) + return new(error2); // To change the login handle of someone who isn't yourself, you need the MANAGE_USERS permission. - if (user != requestingUser) + if (targetUser != requestingUser) { - if (!requestingUser.HasPermission(UserPermission.MANAGE_USERS)) - throw new UserRequestException($"You need permission to manage users to access this endpoint."); - if (!requestingUser.HigherThan(user)) - throw new UserRequestException($"You are not high enough in the role hierarchy to perform this operation."); + + // Do they have MANAGE_USERS? + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error3)) + return new(error3); + + // Are they higher in the Hierarchy? + if (!CommonResponseHandlerChecks.TryCompareHierarchy(requestingUser, targetUser, out ResponseResultError? error4)) + return new(error4); + } // TEMPORARY SOLUTION // TODO: Replace this with a more elegant solution. I think we should make a new class or struct to hold these types of super normalized strings. bool validHandle = request.NewLoginHandle is null || request.NewLoginHandle.All(letter => char.IsAsciiLetterLower(letter) || char.IsAsciiDigit(letter) || letter == '-'); - if (!validHandle) - throw new UserRequestException($"The handle is invalid. Please make sure you use lowercase letters, numbers, or hyphens."); + return new(new ResponseResultDataInvalidError(nameof(request.NewLoginHandle), request.NewLoginHandle)); + + // Check to make sure the new login handle is unique. + if (request.NewLoginHandle is not null && game.State.Users.TryGetUserByLoginHandle(request.NewLoginHandle, out _)) + return new(new ResponseResultDataCollisionError(nameof(request.NewLoginHandle), request.NewLoginHandle)); - if (!await user.TrySetLoginHandle(request.NewLoginHandle)) - throw new UserRequestException($"Unable to set the login handle to '{request.NewLoginHandle}'. It may not be unique."); + if (!await targetUser.TrySetLoginHandle(request.NewLoginHandle)) + throw new Exception($"Unable to set the login handle to '{request.NewLoginHandle}'. It may not be unique."); - return GenericResponseCreators.CreateUserInfoResponse(requestingUser, user); + return new(GenericResponseCreators.CreateUserInfoResponse(requestingUser, targetUser)); } diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs index 4e657ab..b716939 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs @@ -8,27 +8,33 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserNameRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(SetUserNameRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserNameRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + // Check that they're logged in. We don't know what permissions they need yet. + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - if (!game.TryGetUser(request.GUID, out User? user)) - throw new UserRequestException($"{request.GUID} does not match a user."); + if (!CommonResponseHandlerChecks.TryGetUser(game, request.GUID, out User? targetUser, out ResponseResultError? error2)) + return new(error2); // To change the username of someone who isn't yourself, you need the MANAGE_USERS permission. - if (user != requestingUser) + if (targetUser != requestingUser) { - if (!requestingUser.HasPermission(UserPermission.MANAGE_USERS)) - throw new UserRequestException($"You need permission to manage users to access this endpoint."); - if (!requestingUser.HigherThan(user)) - throw new UserRequestException($"You are not high enough in the role hierarchy to perform this operation."); + + // Do they have MANAGE_USERS? + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error3)) + return new(error3); + + // Are they higher in the Hierarchy? + if (!CommonResponseHandlerChecks.TryCompareHierarchy(requestingUser, targetUser, out ResponseResultError? error4)) + return new(error4); + } - await user.SetName(request.NewName); + await targetUser.SetName(request.NewName); - return GenericResponseCreators.CreateUserInfoResponse(requestingUser, user); + return new(GenericResponseCreators.CreateUserInfoResponse(requestingUser, targetUser)); } diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs index 4a174c9..359a4f0 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs @@ -8,27 +8,33 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserPasswordRequestHandler : JsonRequestHandler { - protected override async Task CreateResponse(SetUserPasswordRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserPasswordRequest request, GameServer game, User? requestingUser) { - if (requestingUser is null) - throw new UserRequestException($"You need to be logged in to access this endpoint."); + // Check that they're logged in. We don't know what permissions they need yet. + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) + return new(error1); - if (!game.TryGetUser(request.GUID, out User? user)) - throw new UserRequestException($"{request.GUID} does not match a user."); + if (!CommonResponseHandlerChecks.TryGetUser(game, request.GUID, out User? targetUser, out ResponseResultError? error2)) + return new(error2); // To change the login handle of someone who isn't yourself, you need the MANAGE_USERS permission. - if (user != requestingUser) + if (targetUser != requestingUser) { - if (!requestingUser.HasPermission(UserPermission.MANAGE_USERS)) - throw new UserRequestException($"You need permission to manage users to access this endpoint."); - if (!requestingUser.HigherThan(user)) - throw new UserRequestException($"You are not high enough in the role hierarchy to perform this operation."); + + // Do they have MANAGE_USERS? + if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error3)) + return new(error3); + + // Are they higher in the Hierarchy? + if (!CommonResponseHandlerChecks.TryCompareHierarchy(requestingUser, targetUser, out ResponseResultError? error4)) + return new(error4); + } - await user.SetPassword(request.NewPassword); + await targetUser.SetPassword(request.NewPassword); - return GenericResponseCreators.CreateUserInfoResponse(requestingUser, user); + return new(GenericResponseCreators.CreateUserInfoResponse(requestingUser, targetUser)); } From 6d9beadaa1e3db98c4c18bc6ab93609ff849e36b Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:27:02 -0500 Subject: [PATCH 15/25] Make JsonRequestHandler use Result-Pattern --- .../RequestHandlers/JsonRequestHandler.cs | 76 +++++++++++++------ .../ResponseResultExceptionError.cs | 2 +- .../ResponseResultRequestError.cs | 13 ++++ .../ResponseResultRequestJsonParseError.cs | 14 ++++ ...ResponseResultRequestJsonWrongTypeError.cs | 14 ++++ ...esponseResultRequestQueryDuplicateError.cs | 11 +++ ...esponseResultRequestQueryMisplacedError.cs | 10 +++ ...sponseResultRequestQueryUnexpectedError.cs | 10 +++ 8 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs diff --git a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs index 24890f3..164a1ff 100644 --- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs @@ -1,9 +1,8 @@ -using Microsoft.AspNetCore.Http.HttpResults; -using PMDDesktop.Requests; +using PMDDesktop.Requests; +using PMDDesktop.Results; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; using PMDDesktop.Utils; -using System.Buffers; using System.Reflection; using System.Text; using System.Text.Json; @@ -33,8 +32,7 @@ public Type GetRequestType() /// /// HttpContext of the request /// - /// - private async Task GetJsonObject(HttpContext context) + private async Task> GetJsonObject(HttpContext context) { JsonObject json; @@ -51,17 +49,23 @@ private async Task GetJsonObject(HttpContext context) else { + JsonNode? node; + try { - json = (JsonObject?)await JsonNode.ParseAsync(context.Request.Body) - ?? throw new Exception("Parsed JsonNode was not a JsonObject or otherwise null."); // TODO: Find a way to do this without throwing + node = await JsonNode.ParseAsync(context.Request.Body); } catch (JsonException e) { - throw new Exception($"Error occurred while parsing JSON: {e.Message}", e); // TODO: Find a way to do this without throwing + return new(new ResponseResultRequestJsonParseError(e)); } // Other exceptions should be unhandled. They'll be caught by HandleRequest as a 500 error. + if (node is not JsonObject parsedJson) + return new(new ResponseResultRequestJsonWrongTypeError(JsonValueKind.Object, node?.GetValueKind())); + + json = parsedJson; + } // Reject any JSON fields that are supposed to be query parameters. @@ -69,7 +73,7 @@ private async Task GetJsonObject(HttpContext context) { if (json.ContainsKey(member.Name)) - throw new Exception($"'{member}' should be passed as a query parameter (url components after the ?) and not inside the JSON object."); // TODO: Find a way to do this without throwing + return new(new ResponseResultRequestQueryMisplacedError(member.Name)); } @@ -82,13 +86,15 @@ private async Task GetJsonObject(HttpContext context) string key = query.Key; // Get the member we're looking at, and remove it from our query list. - MemberInfo member = acceptableQueries.Find((member) => member.Name == key) - ?? throw new Exception($"Wasn't expecting a query with key '{key}'"); // TODO: Find a way to do this without throwing + MemberInfo? member = acceptableQueries.Find((member) => member.Name == key); + if (member is null) + return new(new ResponseResultRequestQueryUnexpectedError(key)); + acceptableQueries.Remove(member); if (query.Value.Count != 1) // We only allow one of each query. { - throw new Exception($"Cannot handle {query.Value.Count} values for {key}"); // TODO: Find a way to do this without throwing + return new(new ResponseResultRequestQueryDuplicateError(key, query.Value.Count)); } // Get that value, now that we know it's a singleton, and parse it into a node. @@ -103,7 +109,7 @@ private async Task GetJsonObject(HttpContext context) } catch (JsonException e) { - throw new Exception($"Error occurred while parsing query '{key}' as JSON: {e.Message}", e); // TODO: Find a way to do this without throwing + return new(new ResponseResultRequestJsonParseError(e, key)); } json.Add(key, node); @@ -114,39 +120,59 @@ private async Task GetJsonObject(HttpContext context) foreach (MemberInfo nullQuery in acceptableQueries) json.Add(nullQuery.Name, null); - return json; + // Finally, try to deserialize the object. + TReq? deserialized; + try + { + deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS); + } + catch(JsonException e) + { + return new(new ResponseResultRequestJsonParseError(e)); + } - } + if (deserialized is null) + return new(new ResponseResultRequestJsonWrongTypeError(JsonValueKind.Object, JsonValueKind.Null)); - public async Task HandleRequest(HttpContext context, GameServer game) - { + return new(deserialized); - JsonObject json; + } - ResponseJsonResult result; + public async Task> DetermineResult(HttpContext context, GameServer game) + { try { - json = await GetJsonObject(context); - - TReq deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS) - ?? throw new Exception($"Deserialized data from {context} was null."); + // This is responsible for all the logic that goes into creating a .NET object to use in the request handler. + Result deserializedResult = await GetJsonObject(context); + if (!deserializedResult.TryResult(out TReq? deserialized, out ResponseResultError? error)) + return new(error); + // This tries to return a User object if we could log them in using their token. if (IRequestHandler.TryGetUserFromContext(game.State.Users, context, out User? user, out UserAccessToken? token)) if (token.NeedsRefresh()) await token.RefreshToken(); - result = await CreateResponse(deserialized, game, user); + // This holds all the actual logic for responding to this specific request. + return await CreateResponse(deserialized, game, user); } catch (Exception e) { - result = new(new ResponseResultExceptionError(e)); + return new(new ResponseResultExceptionError(e)); } + } + + public async Task HandleRequest(HttpContext context, GameServer game) + { + + // Main chunk of the logic is inside DetermineResult. + ResponseJsonResult result = await DetermineResult(context, game); + if (!result.TryResult(out TRes? success, out ResponseResultError? error)) { context.Response.ContentType = "text/plain"; diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs index 546d52f..5b7c974 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs @@ -4,7 +4,7 @@ public sealed class ResponseResultExceptionError(Exception exception) : Response { public Exception Exception { get; private init; } = exception; - public override string GetErrorReason() => $"Server-side exception of type '{Exception.GetType().Name}' with message: {Exception.Message}"; + public override string GetErrorReason() => $"Server-side exception of type '{Exception.GetType().Name}' with message: \"{Exception.Message}\" This is likely a bug with the server and should be reported."; public override int GetReturnCode() => 500; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs new file mode 100644 index 0000000..d4bcc56 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs @@ -0,0 +1,13 @@ +namespace PMDDesktop.Server.RequestHandlers; + +/// +/// Abstract class for all errors that occur because the inputs cannot be understood. +/// +public abstract class ResponseResultRequestError() : ResponseResultError +{ + + public override string GetErrorReason() => $"The data you sent is invalid or could not be parsed: {GetRequestErrorReason()}"; + protected abstract string GetRequestErrorReason(); + public override int GetReturnCode() => 400; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs new file mode 100644 index 0000000..788b802 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultRequestJsonParseError(JsonException jsonException, string? nameOfJson = null) : ResponseResultRequestError +{ + + public JsonException JsonException { get; private init; } = jsonException; + public string? NameOfJson { get; private init; } = nameOfJson; + protected override string GetRequestErrorReason() => NameOfJson is null + ? $"Unable to parse JSON: {JsonException.Message}" + : $"Unable to parse JSON '{NameOfJson}': {JsonException.Message}"; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs new file mode 100644 index 0000000..bc08ef2 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultRequestJsonWrongTypeError(JsonValueKind expectedKind, JsonValueKind? actualKind, string? nameOfJson = null) : ResponseResultRequestError +{ + + public JsonValueKind ExpectedKind { get; private init; } = expectedKind; + public JsonValueKind? ActualKind { get; private init; } = actualKind; + public string? NameOfJson { get; private init; } = nameOfJson; + protected override string GetRequestErrorReason() => + $"Parsed JSON {(NameOfJson is not null ? $"{NameOfJson} " : "")}was not {ExpectedKind}, but instead was {(ActualKind is not null ? ActualKind : "was missing (null)")}."; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs new file mode 100644 index 0000000..bd8e9f3 --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs @@ -0,0 +1,11 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultRequestQueryDuplicateError(string queryKey, int count) : ResponseResultRequestError +{ + + public string Key { get; private init; } = queryKey; + public int Count { get; private init; } = count; + + protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided multiple times ({Count} times); each query parameter should be unique."; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs new file mode 100644 index 0000000..83cb5ce --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs @@ -0,0 +1,10 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultRequestQueryMisplacedError(string queryKey) : ResponseResultRequestError +{ + + public string Key { get; private init; } = queryKey; + + protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided in the JSON, but is expected to be provided as part of the URL query."; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs new file mode 100644 index 0000000..86f186c --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs @@ -0,0 +1,10 @@ +namespace PMDDesktop.Server.RequestHandlers; + +public class ResponseResultRequestQueryUnexpectedError(string queryKey) : ResponseResultRequestError +{ + + public string Key { get; private init; } = queryKey; + + protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided, but is unexpected. Check the spelling of the parameter, or do not include it in your request."; + +} From 6f44722bb40a4cc53167c7cfba941dd83467746b Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:30:44 -0500 Subject: [PATCH 16/25] Rename TResult to TSuccess and document --- .../RequestHandlers/ResponseJsonResult.cs | 19 +++++- .../RequestHandlers/ResponseResult.cs | 21 ++++++- PMDDesktop/Results/Result.cs | 59 +++++++++++++------ 3 files changed, 79 insertions(+), 20 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs index c9c9c39..b9f9d89 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs @@ -5,7 +5,24 @@ namespace PMDDesktop.Server.RequestHandlers; public sealed class ResponseJsonResult : ResponseResult where TJsonResponse : ServerJsonResponse where TResponseError : ResponseResultError { + /// + /// Respond that a Request is unsuccessful or errored. + /// + /// + /// Semantically, this means the Request was recieved and our response is that it's unsuccessful. + /// If inside , the provided will be sent back to the client. + /// + /// The to send back to the client. public ResponseJsonResult(TResponseError error) : base(error) { } - public ResponseJsonResult(TJsonResponse result) : base(result) { } + + /// + /// Respond that a Request is successful and this is the object produced by the result. + /// + /// + /// Semantically, this means the Request was recieved and we were able to complete the request. + /// If inside , the provided will be sent back to the client. + /// + /// The to send back to the client. + public ResponseJsonResult(TJsonResponse success) : base(success) { } } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs index 701025a..c9c56cd 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResult.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs @@ -2,10 +2,27 @@ namespace PMDDesktop.Server.RequestHandlers; -public class ResponseResult : Result where TError : ResponseResultError +public class ResponseResult : Result where TError : ResponseResultError { + /// + /// Respond that a Request is unsuccessful or errored. + /// + /// + /// Semantically, this means the Request was recieved and our response is that it's unsuccessful. + /// We cannot take action due to some circumstance, like bad state, bad request, or no permissions. + /// + /// The to hold. public ResponseResult(TError error) : base(error) { } - public ResponseResult(TResult result) : base(result) { } + + /// + /// Respond that a Request is successful and action was taken or info was returned. + /// + /// + /// Semantically, this means the Request was recieved and we were able to complete the request. + /// If for some reason, the request could not be completed, an error should be returned instead. + /// + /// The to hold. + public ResponseResult(TSuccess success) : base(success) { } } diff --git a/PMDDesktop/Results/Result.cs b/PMDDesktop/Results/Result.cs index 820b1c7..14e353f 100644 --- a/PMDDesktop/Results/Result.cs +++ b/PMDDesktop/Results/Result.cs @@ -8,40 +8,65 @@ namespace PMDDesktop.Results; /// Use this for any function that may error but where try-catch can be avoided. /// Use to get the result/error in a "try-pattern." /// -/// +/// /// -public class Result where TError : ResultError +public class Result where TError : ResultError { - private TResult? result; - private TError? error; + /// + /// The output on success, if it exists. + /// + public TSuccess? Success { get; private init; } + /// + /// The output on error, if it exists. + /// + public TError? Error { get; private init; } + + /// + /// Create a new returning as an error. + /// + /// The to hold. public Result(TError error) { - this.error = error; + Error = error; } - public Result(TResult result) + /// + /// Create a new returning as success. + /// + /// The to hold. + public Result(TSuccess result) { - this.result = result; + Success = result; } - public bool HasResult() => error is not null; - public bool HasError() => result is not null; + /// + /// Does this contain a success? + /// + /// True if success is not null. + public bool HasSuccess() => Error is not null; + + /// + /// Does this contain an error? + /// + /// True if error is not null. + public bool HasError() => Success is not null; /// - /// + /// Get the result or the error using a "try-pattern"-like function. + /// This doesn't actually execute the function that creates the result, it only gives you the existing result in a convenient form. /// - /// - /// - /// - public bool TryResult([NotNullWhen(true)] out TResult? result, [NotNullWhen(false)] out TError? error) + /// The existing success, not null if true. + /// The existing error, not null if false. + /// True if Success; false if Error. + public bool TryResult([NotNullWhen(true)] out TSuccess? success, [NotNullWhen(false)] out TError? error) { - result = this.result; - error = this.error; + success = Success; + error = Error; - return result is not null; + return success is not null; } From b6de6e3fcd0e05e8d78f5cce84dfeb0f03e2444e Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:55:31 -0500 Subject: [PATCH 17/25] Remove ResponseJsonResult for ResponseResult --- .../Auth/LoginRequestHandler.cs | 2 +- .../Auth/WhoAmIRequestHandler.cs | 2 +- .../RequestHandlers/JsonRequestHandler.cs | 18 ++++++------ .../ListPermissionsRequestHandler.cs | 2 +- .../ListPokemonTypesRequestHandler.cs | 2 +- .../RequestHandlers/ResponseJsonResult.cs | 28 ------------------- .../Roles/CreateRoleRequestHandler.cs | 2 +- .../Roles/GetRoleInfoRequestHandler.cs | 2 +- .../Roles/ListRolesRequestHandler.cs | 2 +- .../Roles/SetRoleNameRequestHandler.cs | 2 +- .../Roles/SetRolePermissionRequestHandler.cs | 2 +- .../Species/ListSpeciesRequestHandler.cs | 2 +- .../Users/CreateUserRequestHandler.cs | 2 +- .../Users/GetUserInfoRequestHandler.cs | 2 +- .../Users/ListUsersRequestHandler.cs | 2 +- .../Users/SetUserLoginHandleRequestHandler.cs | 2 +- .../Users/SetUserNameRequestHandler.cs | 2 +- .../Users/SetUserPasswordRequestHandler.cs | 2 +- 18 files changed, 25 insertions(+), 53 deletions(-) delete mode 100644 PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs diff --git a/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs index bb412e4..a91e81d 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Auth; public class LoginRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(LoginRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(LoginRequest request, GameServer game, User? requestingUser) { if (!game.State.Users.TryGetUserByLoginHandle(request.Handle, out User? user)) diff --git a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs index b996e33..46f1d76 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Auth; public class WhoAmIRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(WhoAmIRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error)) diff --git a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs index 164a1ff..4af0270 100644 --- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs @@ -1,12 +1,12 @@ -using PMDDesktop.Requests; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using PMDDesktop.Requests; using PMDDesktop.Results; using PMDDesktop.Server.Game; using PMDDesktop.Server.Users; using PMDDesktop.Utils; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; namespace PMDDesktop.Server.RequestHandlers; @@ -126,7 +126,7 @@ private async Task> GetJsonObject(HttpContext { deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS); } - catch(JsonException e) + catch (JsonException e) { return new(new ResponseResultRequestJsonParseError(e)); } @@ -138,7 +138,7 @@ private async Task> GetJsonObject(HttpContext } - public async Task> DetermineResult(HttpContext context, GameServer game) + public async Task> DetermineResult(HttpContext context, GameServer game) { try @@ -171,7 +171,7 @@ public async Task HandleRequest(HttpContext context, GameServer game) { // Main chunk of the logic is inside DetermineResult. - ResponseJsonResult result = await DetermineResult(context, game); + ResponseResult result = await DetermineResult(context, game); if (!result.TryResult(out TRes? success, out ResponseResultError? error)) { @@ -191,6 +191,6 @@ public async Task HandleRequest(HttpContext context, GameServer game) } - protected abstract Task> CreateResponse(TReq request, GameServer game, User? requestingUser); + protected abstract Task> CreateResponse(TReq request, GameServer game, User? requestingUser); } diff --git a/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs index 13b0ffd..0defe9c 100644 --- a/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Permissions; public class ListPermissionsRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(ListPermissionsRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListPermissionsRequest request, GameServer game, User? requestingUser) { return new(new ListPermissionsResponse() diff --git a/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs index 398cc43..9b544a7 100644 --- a/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.PokemonTypes; public class ListPokemonTypesRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser) { return new(new ListPokemonTypesResponse() diff --git a/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs deleted file mode 100644 index b9f9d89..0000000 --- a/PMDDesktop.Server/RequestHandlers/ResponseJsonResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -using PMDDesktop.Requests; - -namespace PMDDesktop.Server.RequestHandlers; - -public sealed class ResponseJsonResult : ResponseResult where TJsonResponse : ServerJsonResponse where TResponseError : ResponseResultError -{ - - /// - /// Respond that a Request is unsuccessful or errored. - /// - /// - /// Semantically, this means the Request was recieved and our response is that it's unsuccessful. - /// If inside , the provided will be sent back to the client. - /// - /// The to send back to the client. - public ResponseJsonResult(TResponseError error) : base(error) { } - - /// - /// Respond that a Request is successful and this is the object produced by the result. - /// - /// - /// Semantically, this means the Request was recieved and we were able to complete the request. - /// If inside , the provided will be sent back to the client. - /// - /// The to send back to the client. - public ResponseJsonResult(TJsonResponse success) : base(success) { } - -} diff --git a/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs index 43a85bf..f81d873 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class CreateRoleRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(CreateRoleRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(CreateRoleRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error)) diff --git a/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs index fc03424..4107034 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/GetRoleInfoRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class GetRoleInfoRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(GetRoleInfoRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(GetRoleInfoRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) diff --git a/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs index 5697e10..249e797 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class ListRolesRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(ListRolesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListRolesRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error)) diff --git a/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs index a5b5b58..aa5a5ab 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/SetRoleNameRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class SetRoleNameRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(SetRoleNameRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetRoleNameRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error1)) diff --git a/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs index e2d2c28..8f30c54 100644 --- a/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Roles/SetRolePermissionRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Roles; public class SetRolePermissionRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(SetRolePermissionRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetRolePermissionRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_ROLES], out ResponseResultError? error1)) diff --git a/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs index 9042ced..aa4eadb 100644 --- a/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Species; public class ListSpeciesRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser) { return new(new ListSpeciesResponse() diff --git a/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs index 8df4628..4088af3 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class CreateUserRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(CreateUserRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(CreateUserRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [UserPermission.MANAGE_USERS], out ResponseResultError? error)) diff --git a/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs index 390ed9b..20952c8 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/GetUserInfoRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class GetUserInfoRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(GetUserInfoRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(GetUserInfoRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) diff --git a/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs index ac06e69..1d05222 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/ListUsersRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class ListUsersRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(ListUsersRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(ListUsersRequest request, GameServer game, User? requestingUser) { if (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error1)) diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs index a9519a4..fe1f325 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserLoginHandleRequestHandler.cs @@ -7,7 +7,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserLoginHandleRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(SetUserLoginHandleRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserLoginHandleRequest request, GameServer game, User? requestingUser) { // Check that they're logged in. We don't know what permissions they need yet. diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs index b716939..4ca20ad 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserNameRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserNameRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(SetUserNameRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserNameRequest request, GameServer game, User? requestingUser) { // Check that they're logged in. We don't know what permissions they need yet. diff --git a/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs index 359a4f0..158b5a6 100644 --- a/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/Users/SetUserPasswordRequestHandler.cs @@ -8,7 +8,7 @@ namespace PMDDesktop.Server.RequestHandlers.Users; public class SetUserPasswordRequestHandler : JsonRequestHandler { - protected override async Task> CreateResponse(SetUserPasswordRequest request, GameServer game, User? requestingUser) + protected override async Task> CreateResponse(SetUserPasswordRequest request, GameServer game, User? requestingUser) { // Check that they're logged in. We don't know what permissions they need yet. From bff2ee828b36374521084c5c0da68325f924aaf5 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:55:47 -0500 Subject: [PATCH 18/25] Document ResultError --- PMDDesktop/Results/ResultError.cs | 9 +++++++++ PMDDesktop/Results/ResultExceptionError.cs | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/PMDDesktop/Results/ResultError.cs b/PMDDesktop/Results/ResultError.cs index 3d99c0b..2c01640 100644 --- a/PMDDesktop/Results/ResultError.cs +++ b/PMDDesktop/Results/ResultError.cs @@ -1,8 +1,17 @@ namespace PMDDesktop.Results; +/// +/// A describes why a couldn't be completed. +/// You can use to make a new with a instead of a . +/// public abstract class ResultError { + /// + /// A string describing why this error happened. + /// If a causes a user-facing issue, this string should be shown somewhere to explain what happened. + /// + /// public abstract string GetErrorReason(); } diff --git a/PMDDesktop/Results/ResultExceptionError.cs b/PMDDesktop/Results/ResultExceptionError.cs index 4376108..93dcb4e 100644 --- a/PMDDesktop/Results/ResultExceptionError.cs +++ b/PMDDesktop/Results/ResultExceptionError.cs @@ -1,8 +1,15 @@ namespace PMDDesktop.Results; +/// +/// Generic for any error that occurs because of an Exception. +/// +/// public class ResultExceptionError(Exception exception) : ResultError { + /// + /// The exception that resulted in this . + /// public Exception Exception { get; private init; } = exception; public override string GetErrorReason() => $"Exception occurred: '{Exception.Message}'"; From 2d414e12bf3985d7729f284ae7e351588c84f743 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:28:02 -0500 Subject: [PATCH 19/25] Refactor ResponseResultDataMissingError to ResponseResultIndexMissingError This makes the intention of ResponseResultIndexMissingError more clear. It could be kinda confused with the meaning of ResponseResultDataInvalidError originally. --- .../CommonResponseHandlerChecks.cs | 10 +++++----- .../ResponseResultDataMissingError.cs | 12 ------------ .../ResponseResultIndexMissingError.cs | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 17 deletions(-) delete mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs index ffbbe04..bb317d0 100644 --- a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs +++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs @@ -50,7 +50,7 @@ internal static bool TryGetUser(IUserIndexable indexable, Guid guid, [NotNullWhe return true; } - error = new ResponseResultDataMissingError(guid.ToString(), typeof(User)); + error = new ResponseResultIndexMissingError(guid.ToString()); return false; } @@ -64,7 +64,7 @@ internal static bool TryGetRole(IRoleIndexable indexable, Guid guid, [NotNullWhe return true; } - error = new ResponseResultDataMissingError(guid.ToString(), typeof(UserRole)); + error = new ResponseResultIndexMissingError(guid.ToString()); return false; } @@ -78,7 +78,7 @@ internal static bool TryGetPermission(string dataName, [NotNullWhen(true)] out U return true; } - error = new ResponseResultDataMissingError(dataName, typeof(UserPermission)); + error = new ResponseResultIndexMissingError(dataName); return false; } @@ -92,7 +92,7 @@ internal static bool TryGetSaveData(ISaveDataIndexable indexable, Guid gu return true; } - error = new ResponseResultDataMissingError(guid.ToString(), typeof(TData)); + error = new ResponseResultIndexMissingError(guid.ToString()); return false; } @@ -106,7 +106,7 @@ internal static bool TryGetAsset(IAssetIndexable indexable, AssetLocatio return true; } - error = new ResponseResultDataMissingError(location, typeof(TAsset)); + error = new ResponseResultIndexMissingError(location); return false; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs deleted file mode 100644 index 631c5b0..0000000 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultDataMissingError.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace PMDDesktop.Server.RequestHandlers; - -public class ResponseResultDataMissingError(string indexer, Type type) : ResponseResultError -{ - - public string Indexer { get; private init; } = indexer; - public Type Type { get; private init; } = type; - - public override string GetErrorReason() => $"Unable to find a {Type.Name} with '${Indexer}'."; - public override int GetReturnCode() => 400; - -} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs new file mode 100644 index 0000000..2a559ba --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs @@ -0,0 +1,16 @@ +namespace PMDDesktop.Server.RequestHandlers; + +/// +/// A that states a specified object of type cannot be found using the provided . +/// +/// +/// +public class ResponseResultIndexMissingError(string indexer) : ResponseResultError +{ + + public string Indexer { get; private init; } = indexer; + + public override string GetErrorReason() => $"Unable to find a {typeof(Type).Name} with '${Indexer}'."; + public override int GetReturnCode() => 400; + +} From 024ca4bc90ffb9bb8b702820d73b64368a2d3b33 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:34:29 -0500 Subject: [PATCH 20/25] Further documented ResponseResultErrors --- .../Auth/ResponseResultLoginError.cs | 2 +- .../RequestHandlers/JsonRequestHandler.cs | 6 ++-- .../ResponseResultAuthenticationError.cs | 9 +++++- .../ResponseResultDataCollisionError.cs | 14 ++++++++- .../ResponseResultDataInvalidError.cs | 14 ++++++++- .../RequestHandlers/ResponseResultError.cs | 10 +++++- .../ResponseResultExceptionError.cs | 12 ++++++- .../ResponseResultHierarchyError.cs | 8 +++-- .../ResponseResultIndexMissingError.cs | 9 ++++-- .../ResponseResultPermissionError.cs | 10 +++++- .../ResponseResultRequestError.cs | 12 +++++-- .../ResponseResultRequestJsonParseError.cs | 13 ++++++++ ...ResponseResultRequestJsonWrongTypeError.cs | 14 --------- ...nseResultRequestJsonWrongValueKindError.cs | 31 +++++++++++++++++++ ...esponseResultRequestQueryDuplicateError.cs | 12 +++++++ ...esponseResultRequestQueryMisplacedError.cs | 7 +++++ ...sponseResultRequestQueryUnexpectedError.cs | 7 +++++ 17 files changed, 159 insertions(+), 31 deletions(-) delete mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs create mode 100644 PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongValueKindError.cs diff --git a/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs b/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs index ac3cdb5..88ceb11 100644 --- a/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs +++ b/PMDDesktop.Server/RequestHandlers/Auth/ResponseResultLoginError.cs @@ -6,6 +6,6 @@ public sealed class ResponseResultLoginError() : ResponseResultError { public override string GetErrorReason() => $"Invalid handle or password."; - public override int GetReturnCode() => 400; + public override int GetStatusCode() => 400; } diff --git a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs index 4af0270..8e9ff2a 100644 --- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs +++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs @@ -62,7 +62,7 @@ private async Task> GetJsonObject(HttpContext // Other exceptions should be unhandled. They'll be caught by HandleRequest as a 500 error. if (node is not JsonObject parsedJson) - return new(new ResponseResultRequestJsonWrongTypeError(JsonValueKind.Object, node?.GetValueKind())); + return new(new ResponseResultRequestJsonWrongValueKindError(JsonValueKind.Object, node?.GetValueKind())); json = parsedJson; @@ -132,7 +132,7 @@ private async Task> GetJsonObject(HttpContext } if (deserialized is null) - return new(new ResponseResultRequestJsonWrongTypeError(JsonValueKind.Object, JsonValueKind.Null)); + return new(new ResponseResultRequestJsonWrongValueKindError(JsonValueKind.Object, JsonValueKind.Null)); return new(deserialized); @@ -176,7 +176,7 @@ public async Task HandleRequest(HttpContext context, GameServer game) if (!result.TryResult(out TRes? success, out ResponseResultError? error)) { context.Response.ContentType = "text/plain"; - context.Response.StatusCode = error.GetReturnCode(); + context.Response.StatusCode = error.GetStatusCode(); byte[] messageBytes = Encoding.UTF8.GetBytes(error.GetErrorReason()); await context.Response.BodyWriter.WriteAsync(messageBytes); return; diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs index cb66db3..3be89d1 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs @@ -1,8 +1,15 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that specifies that the user is not authenticated or could not be authenticated. (Cannot log in.) +/// +/// Optionally take in the provided token string to state that the specific token string didn't work. public class ResponseResultAuthenticationError(string? providedTokenString) : ResponseResultError { + /// + /// The token string that was provided that failed to log in. + /// public string? ProvidedTokenString { get; private init; } = providedTokenString; public override string GetErrorReason() { @@ -13,6 +20,6 @@ public override string GetErrorReason() } - public override int GetReturnCode() => 401; + public override int GetStatusCode() => 401; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs index f6d168c..c525a27 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs @@ -1,12 +1,24 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states a specified value cannot be used because it clashes with another object in a collection/list. +/// +/// A name that helps the user understand what the clashing value was; usually this is the name of the parameter the user sent. +/// The value that was recieved; this is the string that's causing the clash. Usually a unique ID. public class ResponseResultDataCollisionError(string identifier, string providedData) : ResponseResultError { + /// + /// A name that helps the user understand what the clashing value was; usually this is the name of the parameter the user sent. + /// public string DataIdentifier { get; private init; } = identifier; + + /// + /// The value that was recieved; this is the string that's causing the clash. Usually a unique ID. + /// public string? ProvidedData { get; private init; } = providedData; public override string GetErrorReason() => $"The provided value for ''{DataIdentifier}', '{ProvidedData}', cannot be used because it collides or duplicates with other data."; - public override int GetReturnCode() => 400; + public override int GetStatusCode() => 400; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs index 9b1582a..d3d6232 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs @@ -1,12 +1,24 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states a specified value cannot be used and can never be used because it is invalid. +/// +/// A name that helps the user understand what the invalid value was; usually this is the name of the parameter the user sent. +/// The value that was recieved; this is the string that's invalid. Usually a strict variable. public class ResponseResultDataInvalidError(string identifier, string? providedData) : ResponseResultError { + /// + /// A name that helps the user understand what the invalid value was; usually this is the name of the parameter the user sent. + /// public string DataIdentifier { get; private init; } = identifier; + + /// + /// The value that was recieved; this is the string that's invalid. Usually a strict variable. + /// public string? ProvidedData { get; private init; } = providedData; public override string GetErrorReason() => $"'{ProvidedData}' is not valid for '{DataIdentifier}'."; - public override int GetReturnCode() => 400; + public override int GetStatusCode() => 400; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs index 6a2b38e..dc9657f 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs @@ -2,9 +2,17 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// Represents that the request failed in some way; and details our response about why it failed. +/// Override to specify which return code to return. +/// public abstract class ResponseResultError : ResultError { - public abstract int GetReturnCode(); + /// + /// The HTTP status code to return for this error. Usually somewhere in the 400-599 range. + /// + /// An int representing the HTTP status code for this error, usually between 400-599. + public abstract int GetStatusCode(); } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs index 5b7c974..ba996bb 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs @@ -1,10 +1,20 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that an exception occurred while processing the request. +/// +/// The exception that caused this to be returned. +/// +/// If the exception that occurred can be avoided, but still invalidates the request, try to avoid it and return a different instead. +/// public sealed class ResponseResultExceptionError(Exception exception) : ResponseResultError { + /// + /// The exception that caused this to be returned. + /// public Exception Exception { get; private init; } = exception; public override string GetErrorReason() => $"Server-side exception of type '{Exception.GetType().Name}' with message: \"{Exception.Message}\" This is likely a bug with the server and should be reported."; - public override int GetReturnCode() => 500; + public override int GetStatusCode() => 500; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs index 347936d..bfe0218 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs @@ -1,11 +1,15 @@ -using PMDDesktop.Server.Users.Roles; +using PMDDesktop.Server.Users; +using PMDDesktop.Server.Users.Roles; namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that the user is not high enough in the Role Hierarchy to perform an operation on another . +/// public class ResponseResultHierarchyError() : ResponseResultError { public override string GetErrorReason() => $"You are not high enough in the role hierarchy to perform this operation."; - public override int GetReturnCode() => 403; + public override int GetStatusCode() => 403; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs index 2a559ba..cd2f884 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs @@ -3,14 +3,17 @@ /// /// A that states a specified object of type cannot be found using the provided . /// -/// -/// +/// The ID or name that was used to try to find the object; usually a stringified GUID or handle. +/// The Type of the object that couldn't be found. public class ResponseResultIndexMissingError(string indexer) : ResponseResultError { + /// + /// The ID or name that was used to try to find the object; usually a stringified GUID or handle. + /// public string Indexer { get; private init; } = indexer; public override string GetErrorReason() => $"Unable to find a {typeof(Type).Name} with '${Indexer}'."; - public override int GetReturnCode() => 400; + public override int GetStatusCode() => 400; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs index f55cf46..9460f8c 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs @@ -2,10 +2,18 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that the user does not have the required permission(s) to perform an action. +/// +/// A list of missing s that will be shown to the user. public class ResponseResultPermissionError(params IEnumerable permissions) : ResponseResultError { + /// + /// A array of s that were missing and resulted in this error. + /// public UserPermission[] Permissions { get; private init; } = [.. permissions]; + public override string GetErrorReason() { @@ -17,6 +25,6 @@ public override string GetErrorReason() } - public override int GetReturnCode() => 403; + public override int GetStatusCode() => 403; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs index d4bcc56..955b8fa 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs @@ -1,13 +1,21 @@ namespace PMDDesktop.Server.RequestHandlers; /// -/// Abstract class for all errors that occur because the inputs cannot be understood. +/// Abstract class for all s that occur because the inputs cannot be understood. /// public abstract class ResponseResultRequestError() : ResponseResultError { public override string GetErrorReason() => $"The data you sent is invalid or could not be parsed: {GetRequestErrorReason()}"; + + /// + /// Create a string describing why this request error happened. + /// + /// A string describing why this request error happened. + /// + /// This will be appended after a generic error stating that the data input was invalid. + /// protected abstract string GetRequestErrorReason(); - public override int GetReturnCode() => 400; + public override int GetStatusCode() => 400; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs index 788b802..e8b1308 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs @@ -2,11 +2,24 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A stating that a provided JSON could not be parsed. +/// +/// The that occurred and resulted in this error. +/// Optional name of the json to help with identifying where the problem is. public class ResponseResultRequestJsonParseError(JsonException jsonException, string? nameOfJson = null) : ResponseResultRequestError { + /// + /// The that occurred and resulted in this error. + /// public JsonException JsonException { get; private init; } = jsonException; + + /// + /// Optional name of the json to help with identifying where the problem is. + /// public string? NameOfJson { get; private init; } = nameOfJson; + protected override string GetRequestErrorReason() => NameOfJson is null ? $"Unable to parse JSON: {JsonException.Message}" : $"Unable to parse JSON '{NameOfJson}': {JsonException.Message}"; diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs deleted file mode 100644 index bc08ef2..0000000 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongTypeError.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json; - -namespace PMDDesktop.Server.RequestHandlers; - -public class ResponseResultRequestJsonWrongTypeError(JsonValueKind expectedKind, JsonValueKind? actualKind, string? nameOfJson = null) : ResponseResultRequestError -{ - - public JsonValueKind ExpectedKind { get; private init; } = expectedKind; - public JsonValueKind? ActualKind { get; private init; } = actualKind; - public string? NameOfJson { get; private init; } = nameOfJson; - protected override string GetRequestErrorReason() => - $"Parsed JSON {(NameOfJson is not null ? $"{NameOfJson} " : "")}was not {ExpectedKind}, but instead was {(ActualKind is not null ? ActualKind : "was missing (null)")}."; - -} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongValueKindError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongValueKindError.cs new file mode 100644 index 0000000..d4f75be --- /dev/null +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonWrongValueKindError.cs @@ -0,0 +1,31 @@ +using System.Text.Json; + +namespace PMDDesktop.Server.RequestHandlers; + +/// +/// A that states that the deserialized value kind of an input JSON does not match the expected value kind of the JSON. +/// +/// The we need. +/// The we got. +/// Optional name of the json to help with identifying where the problem is. +public class ResponseResultRequestJsonWrongValueKindError(JsonValueKind expectedKind, JsonValueKind? actualKind, string? nameOfJson = null) : ResponseResultRequestError +{ + + /// + /// The we need. + /// + public JsonValueKind ExpectedKind { get; private init; } = expectedKind; + + /// + /// The we got. + /// + public JsonValueKind? ActualKind { get; private init; } = actualKind; + + /// + /// Optional name of the json to help with identifying where the problem is. + /// + public string? NameOfJson { get; private init; } = nameOfJson; + protected override string GetRequestErrorReason() => + $"Parsed JSON {(NameOfJson is not null ? $"{NameOfJson} " : "")}was not {ExpectedKind}, but instead was {(ActualKind is not null ? ActualKind : "was missing (null)")}."; + +} diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs index bd8e9f3..699c104 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs @@ -1,9 +1,21 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that the query parameters provided in the URL contain multiple of the same key. +/// +/// The name of the key that was duplicated. +/// The amount of times total that the key was provided. public class ResponseResultRequestQueryDuplicateError(string queryKey, int count) : ResponseResultRequestError { + /// + /// The name of the key that was duplicated. + /// public string Key { get; private init; } = queryKey; + + /// + /// The amount of times total that the key was provided. + /// public int Count { get; private init; } = count; protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided multiple times ({Count} times); each query parameter should be unique."; diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs index 83cb5ce..96ccb72 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs @@ -1,8 +1,15 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that a key in the JSON body should've been provided as a URL Query Parameter. +/// +/// The name of the key that should be passed as a URL Query Parameter. public class ResponseResultRequestQueryMisplacedError(string queryKey) : ResponseResultRequestError { + /// + /// The name of the key that should be passed as a URL Query Parameter. + /// public string Key { get; private init; } = queryKey; protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided in the JSON, but is expected to be provided as part of the URL query."; diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs index 86f186c..fd6630b 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs @@ -1,8 +1,15 @@ namespace PMDDesktop.Server.RequestHandlers; +/// +/// A that states that the query parameters provided in the URL contain a key not included in the schema. +/// +/// The name of the key that was unexpectedly provided. public class ResponseResultRequestQueryUnexpectedError(string queryKey) : ResponseResultRequestError { + /// + /// The name of the key that was unexpectedly provided. + /// public string Key { get; private init; } = queryKey; protected override string GetRequestErrorReason() => $"Query parameter '{Key}' was provided, but is unexpected. Check the spelling of the parameter, or do not include it in your request."; From d66dc3548883a96d54c0e057e421ecb833b696c1 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:56:37 -0500 Subject: [PATCH 21/25] Improve ResponseResultHierarchyError message. --- PMDDesktop.Server.Users/IUserHierarchyComparable.cs | 7 +++++++ PMDDesktop.Server.Users/Roles/UserRole.cs | 5 +++++ PMDDesktop.Server.Users/User.cs | 11 ++++++++--- .../RequestHandlers/CommonResponseHandlerChecks.cs | 2 +- .../RequestHandlers/ResponseResultHierarchyError.cs | 5 ++--- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/PMDDesktop.Server.Users/IUserHierarchyComparable.cs b/PMDDesktop.Server.Users/IUserHierarchyComparable.cs index c600a1b..0c34578 100644 --- a/PMDDesktop.Server.Users/IUserHierarchyComparable.cs +++ b/PMDDesktop.Server.Users/IUserHierarchyComparable.cs @@ -15,4 +15,11 @@ public interface IUserHierarchyComparable /// Number indicating the highest place in the hierarchy this user has. int GetHierarchyPosition(); + /// + /// Returns a name that is reasonable to show to clients. + /// This is used in error messages relating to hierarchy comparisons. + /// + /// A name to be displayed to clients. + string GetClientFacingName(); + } diff --git a/PMDDesktop.Server.Users/Roles/UserRole.cs b/PMDDesktop.Server.Users/Roles/UserRole.cs index e78df7c..d437dbb 100644 --- a/PMDDesktop.Server.Users/Roles/UserRole.cs +++ b/PMDDesktop.Server.Users/Roles/UserRole.cs @@ -274,4 +274,9 @@ public int GetCurrentOrderIndex() public int GetHierarchyPosition() => GetCurrentOrderIndex(); + public string GetClientFacingName() + { + return Name; + } + } diff --git a/PMDDesktop.Server.Users/User.cs b/PMDDesktop.Server.Users/User.cs index 917a83e..8adacfb 100644 --- a/PMDDesktop.Server.Users/User.cs +++ b/PMDDesktop.Server.Users/User.cs @@ -1,8 +1,8 @@ -using Microsoft.AspNetCore.Identity; -using PMDDesktop.Server.Users.Roles; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Identity; +using PMDDesktop.Server.Users.Roles; namespace PMDDesktop.Server.Users; @@ -682,6 +682,11 @@ public int GetHierarchyPosition() } + public string GetClientFacingName() + { + return Name; + } + #endregion } diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs index bb317d0..ec43ce3 100644 --- a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs +++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs @@ -116,7 +116,7 @@ internal static bool TryCompareHierarchy(User requestingUser, IUserHierarchyComp if (!requestingUser.HigherThan(expectedLower)) { - error = new ResponseResultHierarchyError(); + error = new ResponseResultHierarchyError(requestingUser, expectedLower); return false; } diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs index bfe0218..5161234 100644 --- a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs +++ b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs @@ -1,15 +1,14 @@ using PMDDesktop.Server.Users; -using PMDDesktop.Server.Users.Roles; namespace PMDDesktop.Server.RequestHandlers; /// /// A that states that the user is not high enough in the Role Hierarchy to perform an operation on another . /// -public class ResponseResultHierarchyError() : ResponseResultError +public class ResponseResultHierarchyError(User user, IUserHierarchyComparable comparedTo) : ResponseResultError { - public override string GetErrorReason() => $"You are not high enough in the role hierarchy to perform this operation."; + public override string GetErrorReason() => $"You ({user.GetClientFacingName()} ({user.GetHierarchyPosition()})) are not high enough in the role hierarchy to perform an operation on {comparedTo.GetClientFacingName()} ({comparedTo.GetHierarchyPosition()})."; public override int GetStatusCode() => 403; } From cb4c2cebe889f0e73c399e8c6fced88a03baf1f0 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:56:37 -0500 Subject: [PATCH 22/25] Document CommonResponseHandlerChecks --- .../CommonResponseHandlerChecks.cs | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs index ec43ce3..e42a8de 100644 --- a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs +++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs @@ -1,6 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using PMDDesktop.Requests.Roles; -using PMDDesktop.Requests.Users; using PMDDesktop.Server.Assets; using PMDDesktop.Server.Saving; using PMDDesktop.Server.Users; @@ -41,6 +39,15 @@ internal static bool TryAuthorizeUser([NotNullWhen(true)] User? user, IEnumerabl } + /// + /// Helper function to call , but additionally output a if no is found. + /// Intended to be used in senarios where a response cannot continue if the cannot be found. + /// + /// A to get the from. + /// The of the you're trying to find. + /// The , if one was found. + /// An , if no was found. + /// True if is not null; False if is not null. internal static bool TryGetUser(IUserIndexable indexable, Guid guid, [NotNullWhen(true)] out User? user, [NotNullWhen(false)] out ResponseResultError? error) { @@ -55,6 +62,15 @@ internal static bool TryGetUser(IUserIndexable indexable, Guid guid, [NotNullWhe } + /// + /// Helper function to call , but additionally output a if no is found. + /// Intended to be used in senarios where a response cannot continue if the cannot be found. + /// + /// A to get the from. + /// The of the you're trying to find. + /// The , if one was found. + /// An , if no was found. + /// True if is not null; False if is not null. internal static bool TryGetRole(IRoleIndexable indexable, Guid guid, [NotNullWhen(true)] out UserRole? role, [NotNullWhen(false)] out ResponseResultError? error) { @@ -69,6 +85,14 @@ internal static bool TryGetRole(IRoleIndexable indexable, Guid guid, [NotNullWhe } + /// + /// Helper function to find a by its , but additionally output a if no is found. + /// Intended to be used in senarios where a response cannot continue if the cannot be found. + /// + /// The corresponding to the you're looking for + /// The , if one was found. + /// An , if no was found. + /// True if is not null; False if is not null. internal static bool TryGetPermission(string dataName, [NotNullWhen(true)] out UserPermission? permission, [NotNullWhen(false)] out ResponseResultError? error) { @@ -83,6 +107,15 @@ internal static bool TryGetPermission(string dataName, [NotNullWhen(true)] out U } + /// + /// Helper function to call , but additionally output a if no is found. + /// Intended to be used in senarios where a response cannot continue if the cannot be found. + /// + /// A to get the from. + /// The of the you're trying to find. + /// The if one was found. + /// An , if no was found. + /// True if is not null; False if is not null. internal static bool TryGetSaveData(ISaveDataIndexable indexable, Guid guid, [NotNullWhen(true)] out TData? data, [NotNullWhen(false)] out ResponseResultError? error) where TData : SaveData { @@ -97,6 +130,15 @@ internal static bool TryGetSaveData(ISaveDataIndexable indexable, Guid gu } + /// + /// Helper function to call , but additionally output a if no is found. + /// Intended to be used in senarios where a response cannot continue if the cannot be found. + /// + /// A to get the from. + /// The of the you're trying to find. + /// The if one was found. + /// An , if no was found. + /// True if is not null; False if is not null. internal static bool TryGetAsset(IAssetIndexable indexable, AssetLocation location, [NotNullWhen(true)] out TAsset? asset, [NotNullWhen(false)] out ResponseResultError? error) where TAsset : Asset { @@ -111,6 +153,13 @@ internal static bool TryGetAsset(IAssetIndexable indexable, AssetLocatio } + /// + /// Compares the to the provided , returning true if is higher, and false + a otherwise. + /// + /// The who needs to be higher than . + /// The that needs to be lower than . + /// An , if was equal or lower than . + /// True if user is higher; False if user is equal or lower + will not be null. internal static bool TryCompareHierarchy(User requestingUser, IUserHierarchyComparable expectedLower, [NotNullWhen(false)] out ResponseResultError? error) { From 6bb39f84c945e81e651bd0593aa03f163d17f17c Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:08:01 -0600 Subject: [PATCH 23/25] Add PMDDesktop.Server.Tests --- .../PMDDesktop.Server.Tests.csproj | 26 ++++++++++++ .../RequestHandlers/IRequestHandlerTests.cs | 42 +++++++++++++++++++ PMDDesktop.Server/PMDDesktop.Server.csproj | 4 ++ PMDDesktop.slnx | 1 + 4 files changed, 73 insertions(+) create mode 100644 PMDDesktop.Server.Tests/PMDDesktop.Server.Tests.csproj create mode 100644 PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs diff --git a/PMDDesktop.Server.Tests/PMDDesktop.Server.Tests.csproj b/PMDDesktop.Server.Tests/PMDDesktop.Server.Tests.csproj new file mode 100644 index 0000000..5123c08 --- /dev/null +++ b/PMDDesktop.Server.Tests/PMDDesktop.Server.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs b/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs new file mode 100644 index 0000000..f15dfe8 --- /dev/null +++ b/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs @@ -0,0 +1,42 @@ +using PMDDesktop.Requests; +using PMDDesktop.Server.RequestHandlers; +using PMDDesktop.Utils; + +namespace PMDDesktop.Server.Tests.RequestHandlers; + +public class IRequestHandlerTests +{ + + /// + /// This test makes sure that every has exactly one corresponding . + /// + [Fact] + public void AllRequestsHaveHandlers() + { + + IEnumerable handlerTypes = TypeUtils.GetInstanceableClassesAssignableTo(typeof(IRequestHandler)); + + List handledRequestTypes = []; + + foreach (Type type in handlerTypes) + { + + IRequestHandler handler = + ((IRequestHandler?)Activator.CreateInstance(type)) + ?? throw new InvalidCastException($"Unable to cast instance of {type} to {typeof(IRequestHandler).FullName}"); + + Type requestType = handler.GetRequestType(); + + handledRequestTypes.Add(requestType); + + } + + Assert.Distinct(handledRequestTypes); + + List allRequests = [.. TypeUtils.GetInstanceableClassesAssignableTo(typeof(ServerRequest<>))]; + + Assert.All(allRequests, (req) => handledRequestTypes.Contains(req)); + + } + +} diff --git a/PMDDesktop.Server/PMDDesktop.Server.csproj b/PMDDesktop.Server/PMDDesktop.Server.csproj index 47aaf46..ef14df2 100644 --- a/PMDDesktop.Server/PMDDesktop.Server.csproj +++ b/PMDDesktop.Server/PMDDesktop.Server.csproj @@ -18,6 +18,10 @@ + + + + diff --git a/PMDDesktop.slnx b/PMDDesktop.slnx index c3e0818..24a66fd 100644 --- a/PMDDesktop.slnx +++ b/PMDDesktop.slnx @@ -2,6 +2,7 @@ + From 8b995ec0e7b4bff095b77a7011e54a9439975af3 Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:42:28 -0600 Subject: [PATCH 24/25] Fixed oversight in IRequestHandlerTests.AllRequestsHaveHandlers I forgot that Assert.All() requires you to actually assert something rather than just return true/false. --- PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs b/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs index f15dfe8..a7735d3 100644 --- a/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs +++ b/PMDDesktop.Server.Tests/RequestHandlers/IRequestHandlerTests.cs @@ -35,7 +35,7 @@ public void AllRequestsHaveHandlers() List allRequests = [.. TypeUtils.GetInstanceableClassesAssignableTo(typeof(ServerRequest<>))]; - Assert.All(allRequests, (req) => handledRequestTypes.Contains(req)); + Assert.All(allRequests, (req) => Assert.Contains(req, handledRequestTypes)); } From 3e4b14c22ce89f007bb51fc12a2fff317aad0c5f Mon Sep 17 00:00:00 2001 From: Weffuls <111707520+Weffuls@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:00:31 -0600 Subject: [PATCH 25/25] Change ServerImageResponse to ServerStreamResponse --- PMDDesktop/Requests/ServerImageResponse.cs | 9 --------- PMDDesktop/Requests/ServerStreamResponse.cs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) delete mode 100644 PMDDesktop/Requests/ServerImageResponse.cs create mode 100644 PMDDesktop/Requests/ServerStreamResponse.cs diff --git a/PMDDesktop/Requests/ServerImageResponse.cs b/PMDDesktop/Requests/ServerImageResponse.cs deleted file mode 100644 index 5741628..0000000 --- a/PMDDesktop/Requests/ServerImageResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace PMDDesktop.Requests; - -public class ServerImageResponse : ServerJsonResponse -{ - - - -} - diff --git a/PMDDesktop/Requests/ServerStreamResponse.cs b/PMDDesktop/Requests/ServerStreamResponse.cs new file mode 100644 index 0000000..18e4ade --- /dev/null +++ b/PMDDesktop/Requests/ServerStreamResponse.cs @@ -0,0 +1,12 @@ +namespace PMDDesktop.Requests; + +/// +/// This class is a special-case in ServerResponses where we need to understand that any type of data could be coming through. +/// Usually this is a raw file or image of some sort, so any functions handling s and s need to populate this object with the stream they have. +/// +public sealed class ServerStreamResponse(Stream stream) : ServerResponse +{ + + public Stream Stream { get; private init; } = stream; + +}