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..a7735d3
--- /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) => Assert.Contains(req, handledRequestTypes));
+
+ }
+
+}
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/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.Server/RequestHandlers/Auth/LoginRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/LoginRequestHandler.cs
index 0d3c6d4..a91e81d 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..88ceb11
--- /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 GetStatusCode() => 400;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Auth/WhoAmIRequestHandler.cs
index 5457b15..46f1d76 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 (!CommonResponseHandlerChecks.TryAuthorizeUser(requestingUser, [], out ResponseResultError? error))
+ return new(error);
- return new()
+ return new(new WhoAmIResponse()
{
GUID = requestingUser.GUID
- };
+ });
}
diff --git a/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs
new file mode 100644
index 0000000..e42a8de
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/CommonResponseHandlerChecks.cs
@@ -0,0 +1,177 @@
+using System.Diagnostics.CodeAnalysis;
+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;
+
+ }
+
+ ///
+ /// 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)
+ {
+
+ if (indexable.TryGetUser(guid, out user))
+ {
+ error = null;
+ return true;
+ }
+
+ error = new ResponseResultIndexMissingError(guid.ToString());
+ return false;
+
+ }
+
+ ///
+ /// 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)
+ {
+
+ if (indexable.TryGetRole(guid, out role))
+ {
+ error = null;
+ return true;
+ }
+
+ error = new ResponseResultIndexMissingError(guid.ToString());
+ return false;
+
+ }
+
+ ///
+ /// 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)
+ {
+
+ if (UserPermission.TryGetPermissionByDataName(dataName, out permission))
+ {
+ error = null;
+ return true;
+ }
+
+ error = new ResponseResultIndexMissingError(dataName);
+ return false;
+
+ }
+
+ ///
+ /// 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
+ {
+
+ if (indexable.TryGetSave(guid, out data))
+ {
+ error = null;
+ return true;
+ }
+
+ error = new ResponseResultIndexMissingError(guid.ToString());
+ return false;
+
+ }
+
+ ///
+ /// 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
+ {
+
+ if (indexable.TryGetAsset(location, out asset))
+ {
+ error = null;
+ return true;
+ }
+
+ error = new ResponseResultIndexMissingError(location);
+ return false;
+
+ }
+
+ ///
+ /// 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)
+ {
+
+ if (!requestingUser.HigherThan(expectedLower))
+ {
+ error = new ResponseResultHierarchyError(requestingUser, expectedLower);
+ return false;
+ }
+
+ error = null;
+ return true;
+
+ }
+
+}
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..8e9ff2a 100644
--- a/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs
+++ b/PMDDesktop.Server/RequestHandlers/JsonRequestHandler.cs
@@ -1,15 +1,16 @@
-using PMDDesktop.Requests;
-using PMDDesktop.Server.Game;
-using PMDDesktop.Server.Users;
-using PMDDesktop.Utils;
-using System.Reflection;
+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;
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;
@@ -31,8 +32,7 @@ public Type GetRequestType()
///
/// HttpContext of the request
///
- ///
- private async Task GetJsonObject(HttpContext context)
+ private async Task> GetJsonObject(HttpContext context)
{
JsonObject json;
@@ -49,17 +49,23 @@ private async Task GetJsonObject(HttpContext context)
else
{
+ JsonNode? node;
+
try
{
- json = (JsonObject?)await JsonNode.ParseAsync(context.Request.Body)
- ?? throw new UserRequestException("Parsed JsonNode was not a JsonObject or otherwise null.");
+ node = await JsonNode.ParseAsync(context.Request.Body);
}
catch (JsonException e)
{
- throw new UserRequestException($"Error occurred while parsing JSON: {e.Message}", e);
+ 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 ResponseResultRequestJsonWrongValueKindError(JsonValueKind.Object, node?.GetValueKind()));
+
+ json = parsedJson;
+
}
// Reject any JSON fields that are supposed to be query parameters.
@@ -67,7 +73,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.");
+ return new(new ResponseResultRequestQueryMisplacedError(member.Name));
}
@@ -80,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 UserRequestException($"Wasn't expecting a query with key '{key}'");
+ 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 UserRequestException($"Cannot handle {query.Value.Count} values for {key}");
+ 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.
@@ -101,7 +109,7 @@ private async Task GetJsonObject(HttpContext context)
}
catch (JsonException e)
{
- throw new UserRequestException($"Error occurred while parsing query '{key}' as JSON: {e.Message}", e);
+ return new(new ResponseResultRequestJsonParseError(e, key));
}
json.Add(key, node);
@@ -112,99 +120,77 @@ private async Task GetJsonObject(HttpContext context)
foreach (MemberInfo nullQuery in acceptableQueries)
json.Add(nullQuery.Name, null);
- return json;
-
- }
-
- public async Task HandleRequest(HttpContext context, GameServer game)
- {
-
- JsonObject json;
-
+ // Finally, try to deserialize the object.
+ TReq? deserialized;
try
{
-
- json = await GetJsonObject(context);
-
+ deserialized = json.Deserialize(AppInfo.NETWORK_JSON_OPTIONS);
}
- catch (UserRequestException e) // Intended for user-facing errors, like bad inputs.
+ catch (JsonException e)
{
-
- await WritePlainText(context, 400, e.Message);
- return;
-
+ return new(new ResponseResultRequestJsonParseError(e));
}
- catch (Exception e)
- {
- await WritePlainText(context, 500, "Internal error while creating json object: " + e.Message);
- return;
+ if (deserialized is null)
+ return new(new ResponseResultRequestJsonWrongValueKindError(JsonValueKind.Object, JsonValueKind.Null));
- }
+ return new(deserialized);
+
+ }
- TReq deserialized;
+ public async Task> DetermineResult(HttpContext context, GameServer game)
+ {
try
{
- 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();
+
+ // This holds all the actual logic for responding to this specific request.
+ return await CreateResponse(deserialized, game, user);
}
catch (Exception e)
{
- await WritePlainText(context, 400, "Error while deserializing JSON: " + e.Message);
- return;
+ return new(new ResponseResultExceptionError(e));
}
- 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);
+ }
- }
- catch (UserRequestException e) // Intended for user-facing errors, like bad inputs,
- {
+ public async Task HandleRequest(HttpContext context, GameServer game)
+ {
- await WritePlainText(context, 400, e.Message);
- return;
+ // Main chunk of the logic is inside DetermineResult.
+ ResponseResult result = await DetermineResult(context, game);
- }
- 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.GetStatusCode();
+ 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/ListSpeciesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/ListSpeciesRequestHandler.cs
deleted file mode 100644
index 70ce47a..0000000
--- a/PMDDesktop.Server/RequestHandlers/ListSpeciesRequestHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using PMDDesktop.Requests;
-using PMDDesktop.Server.Assets.Data;
-using PMDDesktop.Server.Game;
-using PMDDesktop.Server.Users;
-
-namespace PMDDesktop.Server.RequestHandlers;
-
-public class ListSpeciesRequestHandler : JsonRequestHandler
-{
-
- protected override async Task CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser)
- {
-
- return new()
- {
- SpeciesIDs = [.. game.State.Assets.OfType().Select((species) => (string)species.Location)]
- };
-
- }
-
-}
diff --git a/PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs
deleted file mode 100644
index 56092e4..0000000
--- a/PMDDesktop.Server/RequestHandlers/ListTypesRequestHandler.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using PMDDesktop.Requests;
-using PMDDesktop.Server.Assets.Data;
-using PMDDesktop.Server.Game;
-using PMDDesktop.Server.Users;
-
-namespace PMDDesktop.Server.RequestHandlers;
-
-public class ListTypesRequestHandler : JsonRequestHandler
-{
-
- protected override async Task CreateResponse(ListTypesRequest request, GameServer game, User? requestingUser)
- {
-
- return new()
- {
- Types = [.. game.State.Assets.OfType().Select(TypeAssetToTypeEntry)]
- };
-
- }
-
- private static ListTypesResponse.TypeEntry TypeAssetToTypeEntry(PokemonType type)
- {
-
- return new()
- {
- ID = type.Location,
- Immunities = [.. type.Immunities.Select((reference) => reference.Location.ToString())],
- Weaknesses = [.. type.Weaknesses.Select((reference) => reference.Location.ToString())],
- Resistances = [.. type.Resistances.Select((reference) => reference.Location.ToString())]
- };
-
- }
-
-}
diff --git a/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Permissions/ListPermissionsRequestHandler.cs
index fafd792..0defe9c 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)]
+ });
}
diff --git a/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs
new file mode 100644
index 0000000..9b544a7
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/PokemonTypes/ListPokemonTypesRequestHandler.cs
@@ -0,0 +1,34 @@
+using PMDDesktop.Requests.PokemonTypes;
+using PMDDesktop.Server.Assets.Data;
+using PMDDesktop.Server.Game;
+using PMDDesktop.Server.Users;
+
+namespace PMDDesktop.Server.RequestHandlers.PokemonTypes;
+
+public class ListPokemonTypesRequestHandler : JsonRequestHandler
+{
+
+ protected override async Task> CreateResponse(ListPokemonTypesRequest request, GameServer game, User? requestingUser)
+ {
+
+ return new(new ListPokemonTypesResponse()
+ {
+ Types = [.. game.State.Assets.OfType().Select(TypeAssetToTypeEntry)]
+ });
+
+ }
+
+ private static ListPokemonTypesResponse.TypeEntry TypeAssetToTypeEntry(PokemonType type)
+ {
+
+ return new()
+ {
+ ID = type.Location,
+ Immunities = [.. type.Immunities.Select((reference) => reference.Location.ToString())],
+ Weaknesses = [.. type.Weaknesses.Select((reference) => reference.Location.ToString())],
+ Resistances = [.. type.Resistances.Select((reference) => reference.Location.ToString())]
+ };
+
+ }
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResult.cs b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs
new file mode 100644
index 0000000..c9c56cd
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResult.cs
@@ -0,0 +1,28 @@
+using PMDDesktop.Results;
+
+namespace PMDDesktop.Server.RequestHandlers;
+
+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) { }
+
+ ///
+ /// 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.Server/RequestHandlers/ResponseResultAuthenticationError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs
new file mode 100644
index 0000000..3be89d1
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultAuthenticationError.cs
@@ -0,0 +1,25 @@
+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()
+ {
+
+ 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 GetStatusCode() => 401;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs
new file mode 100644
index 0000000..c525a27
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataCollisionError.cs
@@ -0,0 +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 GetStatusCode() => 400;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs
new file mode 100644
index 0000000..d3d6232
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultDataInvalidError.cs
@@ -0,0 +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 GetStatusCode() => 400;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs
new file mode 100644
index 0000000..dc9657f
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultError.cs
@@ -0,0 +1,18 @@
+using PMDDesktop.Results;
+
+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
+{
+
+ ///
+ /// 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
new file mode 100644
index 0000000..ba996bb
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultExceptionError.cs
@@ -0,0 +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 GetStatusCode() => 500;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs
new file mode 100644
index 0000000..5161234
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultHierarchyError.cs
@@ -0,0 +1,14 @@
+using PMDDesktop.Server.Users;
+
+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(User user, IUserHierarchyComparable comparedTo) : ResponseResultError
+{
+
+ 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;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs
new file mode 100644
index 0000000..cd2f884
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultIndexMissingError.cs
@@ -0,0 +1,19 @@
+namespace PMDDesktop.Server.RequestHandlers;
+
+///
+/// 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 GetStatusCode() => 400;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs
new file mode 100644
index 0000000..9460f8c
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultPermissionError.cs
@@ -0,0 +1,30 @@
+using PMDDesktop.Server.Users.Roles;
+
+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()
+ {
+
+ 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 GetStatusCode() => 403;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs
new file mode 100644
index 0000000..955b8fa
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestError.cs
@@ -0,0 +1,21 @@
+namespace PMDDesktop.Server.RequestHandlers;
+
+///
+/// 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 GetStatusCode() => 400;
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs
new file mode 100644
index 0000000..e8b1308
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestJsonParseError.cs
@@ -0,0 +1,27 @@
+using System.Text.Json;
+
+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/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
new file mode 100644
index 0000000..699c104
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryDuplicateError.cs
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 0000000..96ccb72
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryMisplacedError.cs
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000..fd6630b
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/ResponseResultRequestQueryUnexpectedError.cs
@@ -0,0 +1,17 @@
+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.";
+
+}
diff --git a/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Roles/CreateRoleRequestHandler.cs
index 1bcab1f..f81d873 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..4107034 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 53%
rename from PMDDesktop.Server/RequestHandlers/Roles/ListUsersRequestHandler.cs
rename to PMDDesktop.Server/RequestHandlers/Roles/ListRolesRequestHandler.cs
index 6d2de5a..249e797 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..aa5a5ab 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..8f30c54 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));
}
diff --git a/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs
new file mode 100644
index 0000000..aa4eadb
--- /dev/null
+++ b/PMDDesktop.Server/RequestHandlers/Species/ListSpeciesRequestHandler.cs
@@ -0,0 +1,20 @@
+using PMDDesktop.Requests.Species;
+using PMDDesktop.Server.Game;
+using PMDDesktop.Server.Users;
+
+namespace PMDDesktop.Server.RequestHandlers.Species;
+
+public class ListSpeciesRequestHandler : JsonRequestHandler
+{
+
+ protected override async Task> CreateResponse(ListSpeciesRequest request, GameServer game, User? requestingUser)
+ {
+
+ return new(new ListSpeciesResponse()
+ {
+ SpeciesIDs = [.. game.State.Assets.OfType().Select((species) => (string)species.Location)]
+ });
+
+ }
+
+}
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)
-{
-
-
-
-}
diff --git a/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs b/PMDDesktop.Server/RequestHandlers/Users/CreateUserRequestHandler.cs
index 7246c19..4088af3 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..20952c8 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..1d05222 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..fe1f325 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..4ca20ad 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..158b5a6 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));
}
diff --git a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs
index f99eda7..5504c45 100644
--- a/PMDDesktop.Tests/Utils/TypeUtilsTests.cs
+++ b/PMDDesktop.Tests/Utils/TypeUtilsTests.cs
@@ -1,4 +1,6 @@
using PMDDesktop.Requests;
+using PMDDesktop.Requests.PokemonTypes;
+using PMDDesktop.Requests.Species;
using PMDDesktop.Structs;
using PMDDesktop.Utils;
using System.Reflection;
@@ -78,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.slnx b/PMDDesktop.slnx
index c3e0818..24a66fd 100644
--- a/PMDDesktop.slnx
+++ b/PMDDesktop.slnx
@@ -2,6 +2,7 @@
+
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/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/ListSpeciesResponse.cs b/PMDDesktop/Requests/ListSpeciesResponse.cs
deleted file mode 100644
index a38970b..0000000
--- a/PMDDesktop/Requests/ListSpeciesResponse.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace PMDDesktop.Requests;
-
-public class ListSpeciesResponse : ServerResponse
-{
-
- public required string[] SpeciesIDs { get; init; }
-
-}
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/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/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 73%
rename from PMDDesktop/Requests/ListTypesResponse.cs
rename to PMDDesktop/Requests/PokemonTypes/ListPokemonTypesResponse.cs
index c1a85e2..1e08890 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 : 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
deleted file mode 100644
index a025718..0000000
--- a/PMDDesktop/Requests/ServerImageResponse.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace PMDDesktop.Requests;
-
-public class ServerImageResponse : ServerResponse
-{
-
-
-
-}
-
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/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;
+
+}
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/Species/ListSpeciesResponse.cs b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs
new file mode 100644
index 0000000..3173342
--- /dev/null
+++ b/PMDDesktop/Requests/Species/ListSpeciesResponse.cs
@@ -0,0 +1,8 @@
+namespace PMDDesktop.Requests.Species;
+
+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; }
diff --git a/PMDDesktop/Results/Result.cs b/PMDDesktop/Results/Result.cs
new file mode 100644
index 0000000..14e353f
--- /dev/null
+++ b/PMDDesktop/Results/Result.cs
@@ -0,0 +1,73 @@
+
+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
+{
+
+ ///
+ /// 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)
+ {
+ Error = error;
+ }
+
+ ///
+ /// Create a new returning as success.
+ ///
+ /// The to hold.
+ public Result(TSuccess result)
+ {
+ Success = result;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// 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)
+ {
+
+ success = Success;
+ error = Error;
+
+ return success is not null;
+
+ }
+
+}
diff --git a/PMDDesktop/Results/ResultError.cs b/PMDDesktop/Results/ResultError.cs
new file mode 100644
index 0000000..2c01640
--- /dev/null
+++ b/PMDDesktop/Results/ResultError.cs
@@ -0,0 +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
new file mode 100644
index 0000000..93dcb4e
--- /dev/null
+++ b/PMDDesktop/Results/ResultExceptionError.cs
@@ -0,0 +1,17 @@
+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}'";
+
+}