// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using System.Net; using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Foxnouns.Backend; public class FoxnounsError(string message, Exception? inner = null) : Exception(message) { public Exception? Inner => inner; public class DatabaseError(string message, Exception? inner = null) : FoxnounsError(message, inner); public class UnknownEntityError(Type entityType, Exception? inner = null) : DatabaseError($"Entity of type {entityType.Name} not found", inner); public class RemoteAuthError(string message, string? errorBody = null, Exception? inner = null) : FoxnounsError(message, inner) { public string? ErrorBody => errorBody; public override string ToString() => $"{Message}: {ErrorBody} {(Inner != null ? $"({Inner})" : "")}"; } } public class ApiError( string message, HttpStatusCode? statusCode = null, ErrorCode? errorCode = null ) : FoxnounsError(message) { public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) : ApiError(message, HttpStatusCode.Unauthorized, errorCode); public class Forbidden( string message, IEnumerable? scopes = null, ErrorCode errorCode = ErrorCode.Forbidden ) : ApiError(message, HttpStatusCode.Forbidden, errorCode) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } public class BadRequest( string message, IReadOnlyDictionary>? errors = null ) : ApiError(message, HttpStatusCode.BadRequest) { public BadRequest(string message, string field, object? actualValue) : this( "Error validating input", new Dictionary> { { field, [ValidationError.GenericValidationError(message, actualValue)] }, } ) { } public JObject ToJson() { var o = new JObject { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, { "code", "BAD_REQUEST" }, }; if (errors == null) return o; var a = new JArray(); foreach (KeyValuePair> error in errors) { var errorObj = new JObject { { "key", error.Key }, { "errors", JArray.FromObject(error.Value) }, }; a.Add(errorObj); } o.Add("errors", a); return o; } } /// /// A special version of BadRequest that ASP.NET generates when it encounters an invalid request. /// Any other methods should use instead. /// public class AspBadRequest(string message, ModelStateDictionary? modelState = null) : ApiError(message, HttpStatusCode.BadRequest) { public JObject ToJson() { var o = new JObject { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, { "code", "BAD_REQUEST" }, }; if (modelState == null) return o; var a = new JArray(); foreach ( KeyValuePair error in modelState.Where(e => e.Value is { Errors.Count: > 0 } ) ) { var errorObj = new JObject { { "key", error.Key }, { "errors", new JArray( error.Value!.Errors.Select(e => new JObject { { "message", e.ErrorMessage }, }) ) }, }; a.Add(errorObj); } o.Add("errors", a); return o; } } public class NotFound(string message, ErrorCode? code = null) : ApiError(message, HttpStatusCode.NotFound, code); public class AuthenticationError(string message) : ApiError(message, HttpStatusCode.BadRequest); } public enum ErrorCode { InternalServerError, Forbidden, BadRequest, AuthenticationError, AuthenticationRequired, MissingScopes, GenericApiError, UserNotFound, MemberNotFound, AccountAlreadyLinked, LastAuthMethod, } public class ValidationError { public required string Message { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? MinLength { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? MaxLength { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? ActualLength { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public IEnumerable? AllowedValues { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public object? ActualValue { get; init; } public static ValidationError LengthError( string message, int minLength, int maxLength, int actualLength ) => new() { Message = message, MinLength = minLength, MaxLength = maxLength, ActualLength = actualLength, }; public static ValidationError DisallowedValueError( string message, IEnumerable allowedValues, object actualValue ) => new() { Message = message, AllowedValues = allowedValues, ActualValue = actualValue, }; public static ValidationError GenericValidationError(string message, object? actualValue) => new() { Message = message, ActualValue = actualValue }; }