using System.ComponentModel; using Azaion.Common.Extensions; namespace Azaion.Common; public class BusinessException(ExceptionEnum exEnum) : Exception(GetMessage(exEnum)) { private static readonly Dictionary ExceptionDescriptions; static BusinessException() { ExceptionDescriptions = EnumExtensions.GetDescriptions(); } public ExceptionEnum ExceptionEnum { get; set; } = exEnum; /// /// Optional cooldown hint surfaced as a Retry-After response header by the exception /// handler. Used by AccountLocked and LoginRateLimited (AZ-537). /// public int? RetryAfterSeconds { get; init; } public BusinessException(ExceptionEnum exEnum, int retryAfterSeconds) : this(exEnum) { RetryAfterSeconds = retryAfterSeconds; } public static string GetMessage(ExceptionEnum exEnum) => ExceptionDescriptions.GetValueOrDefault(exEnum) ?? exEnum.ToString(); } public enum ExceptionEnum { // AZ-556 — DEPRECATED: no longer thrown by `UserService.ValidateUser`. The login // path now uses `InvalidCredentials` (70) for all rejection categories to close the // user-enumeration leak (F-AUTH-1 + F-AUTH-3). Kept defined for any cross-workspace // verifier that still pattern-matches on the old codes. Removal is scheduled in a // separate ticket after the deprecation window. [Description("No such email found.")] NoEmailFound = 10, [Description("Email already exists.")] EmailExists = 20, // AZ-556 — DEPRECATED: see the `NoEmailFound` deprecation note above. [Description("Passwords do not match.")] WrongPassword = 30, [Description("Password should be at least 12 characters.")] PasswordLengthIncorrect = 32, [Description("Email is empty or invalid.")] EmailLengthIncorrect = 35, WrongEmail = 37, // AZ-556 — DEPRECATED: see the `NoEmailFound` deprecation note above. [Description("User account is disabled.")] UserDisabled = 38, // AZ-556 — DEPRECATED: cycle-2 unifies the lockout response under // `InvalidCredentials` + Retry-After header (AC-7). Kept defined for cross-workspace // verifier compatibility; will be removed alongside `NoEmailFound`/`WrongPassword`. [Description("Account is temporarily locked due to too many failed login attempts.")] AccountLocked = 50, // AZ-556 — DEPRECATED: see the `AccountLocked` deprecation note above. [Description("Too many login attempts. Try again later.")] LoginRateLimited = 51, [Description("Refresh token is invalid, expired, or has been revoked.")] InvalidRefreshToken = 52, [Description("Session not found.")] SessionNotFound = 53, [Description("Mission token request is invalid.")] InvalidMissionRequest = 54, [Description("Aircraft not found or wrong role.")] AircraftNotFound = 55, [Description("MFA is already enabled for this user.")] MfaAlreadyEnabled = 56, [Description("MFA enrollment is not in progress for this user.")] MfaNotEnrolling = 57, [Description("MFA is not enabled for this user.")] MfaNotEnabled = 58, [Description("Invalid MFA code or recovery code.")] InvalidMfaCode = 59, [Description("MFA token is invalid or expired.")] InvalidMfaToken = 61, [Description("No file provided.")] NoFileProvided = 60, // AZ-556 — single opaque login-failure code. Replaces the wire-side use of // `NoEmailFound`, `WrongPassword`, `UserDisabled`, `AccountLocked`, and // `LoginRateLimited`. The audit log preserves the actual category for SecOps. // Lockout / rate-limit responses additionally carry a Retry-After header via // `BusinessException.RetryAfterSeconds`. [Description("Invalid credentials.")] InvalidCredentials = 70, }