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}'"; + +}