using System.Globalization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace Azaion.Common; public class BusinessExceptionHandler(ILogger logger) : IExceptionHandler { public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { if (exception is BusinessException ex) { logger.LogWarning(exception, ex.Message); httpContext.Response.StatusCode = MapStatusCode(ex.ExceptionEnum); httpContext.Response.ContentType = "application/json"; if (ex.RetryAfterSeconds is { } retry && retry > 0) httpContext.Response.Headers.RetryAfter = retry.ToString(CultureInfo.InvariantCulture); var err = JsonConvert.SerializeObject(new { ErrorCode = ex.ExceptionEnum, ex.Message }); await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false); return true; } if (exception is BadHttpRequestException badReq) { logger.LogWarning(exception, badReq.Message); httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; httpContext.Response.ContentType = "application/json"; var err = JsonConvert.SerializeObject(new { ErrorCode = 0, badReq.Message }); await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false); return true; } return false; } private static int MapStatusCode(ExceptionEnum kind) => kind switch { // AZ-556 — `InvalidCredentials` covers unknown email, wrong password, disabled // account, lockout, and per-account rate-limit. Same 401 for all five so the // wire response carries no signal beyond the optional Retry-After header. ExceptionEnum.InvalidCredentials => StatusCodes.Status401Unauthorized, ExceptionEnum.AccountLocked => StatusCodes.Status423Locked, ExceptionEnum.LoginRateLimited => StatusCodes.Status429TooManyRequests, ExceptionEnum.InvalidRefreshToken => StatusCodes.Status401Unauthorized, ExceptionEnum.SessionNotFound => StatusCodes.Status404NotFound, ExceptionEnum.InvalidMissionRequest => StatusCodes.Status400BadRequest, ExceptionEnum.AircraftNotFound => StatusCodes.Status400BadRequest, ExceptionEnum.MfaAlreadyEnabled => StatusCodes.Status409Conflict, ExceptionEnum.MfaNotEnrolling => StatusCodes.Status409Conflict, ExceptionEnum.MfaNotEnabled => StatusCodes.Status409Conflict, ExceptionEnum.InvalidMfaCode => StatusCodes.Status401Unauthorized, ExceptionEnum.InvalidMfaToken => StatusCodes.Status401Unauthorized, _ => StatusCodes.Status409Conflict }; }