🧱 Add new ApiError system
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Pass.Auth;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
|
using DysonNetwork.Shared.Error;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -28,7 +29,7 @@ public class AccountController(
|
|||||||
.Include(e => e.Contacts.Where(c => c.IsPublic))
|
.Include(e => e.Contacts.Where(c => c.IsPublic))
|
||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (account is null) return new NotFoundResult();
|
if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
|
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
|
||||||
account.PerkSubscription = perk?.ToReference();
|
account.PerkSubscription = perk?.ToReference();
|
||||||
@@ -45,7 +46,7 @@ public class AccountController(
|
|||||||
.Include(e => e.Badges)
|
.Include(e => e.Badges)
|
||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
return account is null ? NotFound() : account.Badges.ToList();
|
return account is null ? NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier)) : account.Badges.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountCreateRequest
|
public class AccountCreateRequest
|
||||||
@@ -81,7 +82,11 @@ public class AccountController(
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request)
|
public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request)
|
||||||
{
|
{
|
||||||
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
if (!await auth.ValidateCaptcha(request.CaptchaToken))
|
||||||
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(request.CaptchaToken)] = ["Invalid captcha token."]
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -96,7 +101,14 @@ public class AccountController(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "BAD_REQUEST",
|
||||||
|
Message = "Failed to create account.",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +121,22 @@ public class AccountController(
|
|||||||
[HttpPost("recovery/password")]
|
[HttpPost("recovery/password")]
|
||||||
public async Task<ActionResult> RequestResetPassword([FromBody] RecoveryPasswordRequest request)
|
public async Task<ActionResult> RequestResetPassword([FromBody] RecoveryPasswordRequest request)
|
||||||
{
|
{
|
||||||
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
if (!await auth.ValidateCaptcha(request.CaptchaToken))
|
||||||
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(request.CaptchaToken)] = new[] { "Invalid captcha token." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
var account = await accounts.LookupAccount(request.Account);
|
var account = await accounts.LookupAccount(request.Account);
|
||||||
if (account is null) return BadRequest("Unable to find the account.");
|
if (account is null)
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "NOT_FOUND",
|
||||||
|
Message = "Unable to find the account.",
|
||||||
|
Detail = request.Account,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -120,7 +144,13 @@ public class AccountController(
|
|||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
return BadRequest("You already requested password reset within 24 hours.");
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "TOO_MANY_REQUESTS",
|
||||||
|
Message = "You already requested password reset within 24 hours.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -139,7 +169,15 @@ public class AccountController(
|
|||||||
public async Task<ActionResult<Status>> GetOtherStatus(string name)
|
public async Task<ActionResult<Status>> GetOtherStatus(string name)
|
||||||
{
|
{
|
||||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name);
|
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name);
|
||||||
if (account is null) return BadRequest();
|
if (account is null)
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "NOT_FOUND",
|
||||||
|
Message = "Account not found.",
|
||||||
|
Detail = name,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
var status = await events.GetStatus(account.Id);
|
var status = await events.GetStatus(account.Id);
|
||||||
status.IsInvisible = false; // Keep the invisible field not available for other users
|
status.IsInvisible = false; // Keep the invisible field not available for other users
|
||||||
return Ok(status);
|
return Ok(status);
|
||||||
@@ -156,11 +194,27 @@ public class AccountController(
|
|||||||
month ??= currentDate.Month;
|
month ??= currentDate.Month;
|
||||||
year ??= currentDate.Year;
|
year ??= currentDate.Year;
|
||||||
|
|
||||||
if (month is < 1 or > 12) return BadRequest("Invalid month.");
|
if (month is < 1 or > 12)
|
||||||
if (year < 1) return BadRequest("Invalid year.");
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(month)] = new[] { "Month must be between 1 and 12." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
if (year < 1)
|
||||||
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(year)] = new[] { "Year must be a positive integer." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name);
|
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name);
|
||||||
if (account is null) return BadRequest();
|
if (account is null)
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "not_found",
|
||||||
|
Message = "Account not found.",
|
||||||
|
Detail = name,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
|
|
||||||
var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true);
|
var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true);
|
||||||
return Ok(calendar);
|
return Ok(calendar);
|
||||||
|
@@ -3,6 +3,7 @@ using DysonNetwork.Pass.Auth;
|
|||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.Error;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -92,7 +93,14 @@ public class AccountCurrentController(
|
|||||||
var profile = await db.AccountProfiles
|
var profile = await db.AccountProfiles
|
||||||
.Where(p => p.Account.Id == userId)
|
.Where(p => p.Account.Id == userId)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (profile is null) return BadRequest("Unable to get your account.");
|
if (profile is null)
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "NOT_FOUND",
|
||||||
|
Message = "Unable to get your account.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
|
|
||||||
if (request.FirstName is not null) profile.FirstName = request.FirstName;
|
if (request.FirstName is not null) profile.FirstName = request.FirstName;
|
||||||
if (request.MiddleName is not null) profile.MiddleName = request.MiddleName;
|
if (request.MiddleName is not null) profile.MiddleName = request.MiddleName;
|
||||||
@@ -160,7 +168,13 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
return BadRequest("You already requested account deletion within 24 hours.");
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "TOO_MANY_REQUESTS",
|
||||||
|
Message = "You already requested account deletion within 24 hours.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -186,7 +200,7 @@ public class AccountCurrentController(
|
|||||||
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (status is null) return NotFound();
|
if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
status.Attitude = request.Attitude;
|
status.Attitude = request.Attitude;
|
||||||
status.IsInvisible = request.IsInvisible;
|
status.IsInvisible = request.IsInvisible;
|
||||||
@@ -254,7 +268,7 @@ public class AccountCurrentController(
|
|||||||
.OrderByDescending(x => x.CreatedAt)
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
return result is null ? NotFound() : Ok(result);
|
return result is null ? NotFound(ApiError.NotFound("check-in", traceId: HttpContext.TraceIdentifier)) : Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("check-in")]
|
[HttpPost("check-in")]
|
||||||
@@ -269,15 +283,30 @@ public class AccountCurrentController(
|
|||||||
{
|
{
|
||||||
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
|
||||||
if (!isAvailable)
|
if (!isAvailable)
|
||||||
return BadRequest("Check-in is not available for today.");
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "BAD_REQUEST",
|
||||||
|
Message = "Check-in is not available for today.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (currentUser.PerkSubscription is null)
|
if (currentUser.PerkSubscription is null)
|
||||||
return StatusCode(403, "You need to have a subscription to check-in backdated.");
|
return StatusCode(403, ApiError.Unauthorized(
|
||||||
|
message: "You need to have a subscription to check-in backdated.",
|
||||||
|
forbidden: true,
|
||||||
|
traceId: HttpContext.TraceIdentifier));
|
||||||
var isAvailable = await events.CheckInBackdatedIsAvailable(currentUser, backdated.Value);
|
var isAvailable = await events.CheckInBackdatedIsAvailable(currentUser, backdated.Value);
|
||||||
if (!isAvailable)
|
if (!isAvailable)
|
||||||
return BadRequest("Check-in is not available for this date.");
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "BAD_REQUEST",
|
||||||
|
Message = "Check-in is not available for this date.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -286,15 +315,31 @@ public class AccountCurrentController(
|
|||||||
return needsCaptcha switch
|
return needsCaptcha switch
|
||||||
{
|
{
|
||||||
true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423,
|
true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423,
|
||||||
"Captcha is required for this check-in."
|
new ApiError
|
||||||
|
{
|
||||||
|
Code = "CAPTCHA_REQUIRED",
|
||||||
|
Message = "Captcha is required for this check-in.",
|
||||||
|
Status = 423,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
}
|
||||||
),
|
),
|
||||||
true when !await auth.ValidateCaptcha(captchaToken!) => BadRequest("Invalid captcha token."),
|
true when !await auth.ValidateCaptcha(captchaToken!) => BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
["captchaToken"] = new[] { "Invalid captcha token." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier)),
|
||||||
_ => await events.CheckInDaily(currentUser, backdated)
|
_ => await events.CheckInDaily(currentUser, backdated)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "BAD_REQUEST",
|
||||||
|
Message = "Check-in failed.",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,8 +353,16 @@ public class AccountCurrentController(
|
|||||||
month ??= currentDate.Month;
|
month ??= currentDate.Month;
|
||||||
year ??= currentDate.Year;
|
year ??= currentDate.Year;
|
||||||
|
|
||||||
if (month is < 1 or > 12) return BadRequest("Invalid month.");
|
if (month is < 1 or > 12)
|
||||||
if (year < 1) return BadRequest("Invalid year.");
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(month)] = new[] { "Month must be between 1 and 12." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
if (year < 1)
|
||||||
|
return BadRequest(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[nameof(year)] = new[] { "Year must be a positive integer." }
|
||||||
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
var calendar = await events.GetEventCalendar(currentUser, month.Value, year.Value);
|
var calendar = await events.GetEventCalendar(currentUser, month.Value, year.Value);
|
||||||
return Ok(calendar);
|
return Ok(calendar);
|
||||||
@@ -365,7 +418,13 @@ public class AccountCurrentController(
|
|||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||||
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "ALREADY_EXISTS",
|
||||||
|
Message = $"Auth factor with type {request.Type} already exists.",
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
|
|
||||||
var factor = await accounts.CreateAuthFactor(currentUser, request.Type, request.Secret);
|
var factor = await accounts.CreateAuthFactor(currentUser, request.Type, request.Secret);
|
||||||
return Ok(factor);
|
return Ok(factor);
|
||||||
@@ -380,7 +439,7 @@ public class AccountCurrentController(
|
|||||||
var factor = await db.AccountAuthFactors
|
var factor = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (factor is null) return NotFound();
|
if (factor is null) return NotFound(ApiError.NotFound(id.ToString(), traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -389,7 +448,14 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Code = "BAD_REQUEST",
|
||||||
|
Message = "Failed to enable auth factor.",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = 400,
|
||||||
|
TraceId = HttpContext.TraceIdentifier
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,4 +36,8 @@
|
|||||||
<Protobuf Include="Proto\*.proto" ProtoRoot="Proto" GrpcServices="Both" AdditionalFileExtensions="Proto\" />
|
<Protobuf Include="Proto\*.proto" ProtoRoot="Proto" GrpcServices="Both" AdditionalFileExtensions="Proto\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Error\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
135
DysonNetwork.Shared/Error/ApiError.cs
Normal file
135
DysonNetwork.Shared/Error/ApiError.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Error;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standardized error payload to return to clients.
|
||||||
|
/// Inspired by RFC7807 (problem+json) with app-specific fields.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiError
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Application-specific error code (e.g., "VALIDATION_ERROR", "NOT_FOUND", "SERVER_ERROR").
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public string Code { get; set; } = "UNKNOWN_ERROR";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Short, human-readable message for the error.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = "An unexpected error occurred.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP status code to be used by the server when sending this error.
|
||||||
|
/// Optional to keep the model transport-agnostic.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// More detailed description of the error.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("detail")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Detail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server trace identifier (e.g., from HttpContext.TraceIdentifier) to help debugging.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("traceId")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? TraceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Field-level validation errors: key is the field name, value is an array of messages.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public Dictionary<string, string[]>? Errors { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Arbitrary additional metadata for clients.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("meta")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public Dictionary<string, object?>? Meta { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for a validation error payload.
|
||||||
|
/// </summary>
|
||||||
|
public static ApiError Validation(
|
||||||
|
Dictionary<string, string[]> errors,
|
||||||
|
string? message = null,
|
||||||
|
int status = 400,
|
||||||
|
string code = "VALIDATION_ERROR",
|
||||||
|
string? traceId = null)
|
||||||
|
{
|
||||||
|
return new ApiError
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Message = message ?? "One or more validation errors occurred.",
|
||||||
|
Status = status,
|
||||||
|
Errors = errors,
|
||||||
|
TraceId = traceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for a not-found error payload.
|
||||||
|
/// </summary>
|
||||||
|
public static ApiError NotFound(
|
||||||
|
string resource,
|
||||||
|
string? message = null,
|
||||||
|
int status = 404,
|
||||||
|
string code = "NOT_FOUND",
|
||||||
|
string? traceId = null)
|
||||||
|
{
|
||||||
|
return new ApiError
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Message = message ?? $"The requested resource '{resource}' was not found.",
|
||||||
|
Status = status,
|
||||||
|
Detail = resource,
|
||||||
|
TraceId = traceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for a generic server error payload.
|
||||||
|
/// </summary>
|
||||||
|
public static ApiError Server(
|
||||||
|
string? message = null,
|
||||||
|
int status = 500,
|
||||||
|
string code = "SERVER_ERROR",
|
||||||
|
string? traceId = null,
|
||||||
|
string? detail = null)
|
||||||
|
{
|
||||||
|
return new ApiError
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Message = message ?? "An internal server error occurred.",
|
||||||
|
Status = status,
|
||||||
|
TraceId = traceId,
|
||||||
|
Detail = detail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for an unauthorized/forbidden error payload.
|
||||||
|
/// </summary>
|
||||||
|
public static ApiError Unauthorized(
|
||||||
|
string? message = null,
|
||||||
|
bool forbidden = false,
|
||||||
|
string? traceId = null)
|
||||||
|
{
|
||||||
|
return new ApiError
|
||||||
|
{
|
||||||
|
Code = forbidden ? "FORBIDDEN" : "UNAUTHORIZED",
|
||||||
|
Message = message ?? (forbidden ? "You do not have permission to perform this action." : "Authentication is required."),
|
||||||
|
Status = forbidden ? 403 : 401,
|
||||||
|
TraceId = traceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user