🧱 Add new ApiError system

This commit is contained in:
2025-08-18 01:10:49 +08:00
parent d4a2e5ef5b
commit 201126e5d0
4 changed files with 285 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Error;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
@@ -28,7 +29,7 @@ public class AccountController(
.Include(e => e.Contacts.Where(c => c.IsPublic))
.Where(a => a.Name == name)
.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);
account.PerkSubscription = perk?.ToReference();
@@ -45,7 +46,7 @@ public class AccountController(
.Include(e => e.Badges)
.Where(a => a.Name == name)
.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
@@ -81,7 +82,11 @@ public class AccountController(
[ProducesResponseType(StatusCodes.Status400BadRequest)]
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
{
@@ -96,7 +101,14 @@ public class AccountController(
}
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")]
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);
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
{
@@ -120,7 +144,13 @@ public class AccountController(
}
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();
@@ -139,7 +169,15 @@ public class AccountController(
public async Task<ActionResult<Status>> GetOtherStatus(string 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);
status.IsInvisible = false; // Keep the invisible field not available for other users
return Ok(status);
@@ -156,11 +194,27 @@ public class AccountController(
month ??= currentDate.Month;
year ??= currentDate.Year;
if (month is < 1 or > 12) return BadRequest("Invalid month.");
if (year < 1) return BadRequest("Invalid year.");
if (month is < 1 or > 12)
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);
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);
return Ok(calendar);

View File

@@ -3,6 +3,7 @@ using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Error;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -92,7 +93,14 @@ public class AccountCurrentController(
var profile = await db.AccountProfiles
.Where(p => p.Account.Id == userId)
.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.MiddleName is not null) profile.MiddleName = request.MiddleName;
@@ -160,7 +168,13 @@ public class AccountCurrentController(
}
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();
@@ -186,7 +200,7 @@ public class AccountCurrentController(
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
.OrderByDescending(e => e.CreatedAt)
.FirstOrDefaultAsync();
if (status is null) return NotFound();
if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier));
status.Attitude = request.Attitude;
status.IsInvisible = request.IsInvisible;
@@ -254,7 +268,7 @@ public class AccountCurrentController(
.OrderByDescending(x => x.CreatedAt)
.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")]
@@ -269,15 +283,30 @@ public class AccountCurrentController(
{
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
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
{
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);
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
@@ -286,15 +315,31 @@ public class AccountCurrentController(
return needsCaptcha switch
{
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)
};
}
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;
year ??= currentDate.Year;
if (month is < 1 or > 12) return BadRequest("Invalid month.");
if (year < 1) return BadRequest("Invalid year.");
if (month is < 1 or > 12)
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);
return Ok(calendar);
@@ -365,7 +418,13 @@ public class AccountCurrentController(
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
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);
return Ok(factor);
@@ -380,7 +439,7 @@ public class AccountCurrentController(
var factor = await db.AccountAuthFactors
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
.FirstOrDefaultAsync();
if (factor is null) return NotFound();
if (factor is null) return NotFound(ApiError.NotFound(id.ToString(), traceId: HttpContext.TraceIdentifier));
try
{
@@ -389,7 +448,14 @@ public class AccountCurrentController(
}
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
});
}
}