Files
admin/_docs/02_document/modules/common_business_exception.md
T
Oleksandr Bezdieniezhnykh a77b3f8a59 [AZ-529] [AZ-530] Cycle-2 documentation refresh
Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.

Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
  /mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}

Step 13 (Update Docs) output for cycle 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:22:53 +03:00

5.6 KiB

Module: Azaion.Common.BusinessException

Purpose

Custom exception type for domain-level errors, paired with an ExceptionEnum catalog of all business error codes.

Public Interface

BusinessException

Member Signature Description
Constructor BusinessException(ExceptionEnum exEnum) Creates exception with message from ExceptionEnum's [Description] attribute
ExceptionEnum ExceptionEnum ExceptionEnum { get; set; } The specific error code
GetMessage static string GetMessage(ExceptionEnum exEnum) Looks up human-readable message for an error code

ExceptionEnum

Value Code Description HTTP Status
NoEmailFound 10 No such email found 409
EmailExists 20 Email already exists 409
WrongPassword 30 Passwords do not match 409
PasswordLengthIncorrect 32 Password should be at least 12 characters 409
EmailLengthIncorrect 35 Email is empty or invalid 409
WrongEmail 37 (no description attribute) 409
UserDisabled 38 User account is disabled 409
AccountLocked 50 AZ-537 — account temporarily locked due to too many failed login attempts (carries RetryAfterSeconds) 423 Locked
LoginRateLimited 51 AZ-537 — too many login attempts per account; try again later (carries RetryAfterSeconds) 429 Too Many Requests
InvalidRefreshToken 52 AZ-531 — refresh token invalid / expired / revoked / reuse-detected 401 Unauthorized
SessionNotFound 53 AZ-535 — admin tried to revoke a non-existent session 404 Not Found
InvalidMissionRequest 54 AZ-533 — mission_id pattern fail or planned_duration_h out of bounds 400 Bad Request
AircraftNotFound 55 AZ-533 — aircraft id missing or not a CompanionPC user 400 Bad Request
MfaAlreadyEnabled 56 AZ-534 — /users/me/mfa/enroll called for a user that already has MFA on 409 Conflict
MfaNotEnrolling 57 AZ-534 — confirm called without a prior enroll 409 Conflict
MfaNotEnabled 58 AZ-534 — disable / verify-for-login called for a user without MFA 409 Conflict
InvalidMfaCode 59 AZ-534 — TOTP code (and recovery code) failed to verify 401 Unauthorized
NoFileProvided 60 No file provided 409
InvalidMfaToken 61 AZ-534 — step-1 MFA token failed to validate (signature / audience / expiry) 401 Unauthorized

RetryAfterSeconds

Member Type Description
Constructor BusinessException(ExceptionEnum exEnum, int retryAfterSeconds) Cycle 2 (AZ-537) — sets RetryAfterSeconds, surfaced by BusinessExceptionHandler as a Retry-After response header. Used by AccountLocked (returns remaining lockout seconds) and LoginRateLimited (returns the window seconds).
RetryAfterSeconds int? Optional cooldown hint; null when the exception was constructed without a window.

Cycle 1 (2026-05-13) noteHardwareIdMismatch = 40 and BadHardware = 45 were removed by AZ-197. Codes 40 and 45 should NOT be reused.

Cycle 2 (2026-05-14) noteWrongResourceName = 50 was removed early in the cycle along with the GetResourceRequest validator. The integer 50 has since been reused for AccountLocked as part of AZ-537 (since the previous user-facing string "Wrong resource name" is no longer surfaced anywhere). This is the one deliberate exception to the "gap kept" lesson — the old code had no remaining client surface and the auth modernization wanted a tightly-clustered range of new codes.

Internal Logic

Static constructor eagerly loads all ExceptionEnum descriptions into a dictionary via EnumExtensions.GetDescriptions<ExceptionEnum>(). Messages are retrieved by dictionary lookup with fallback to ToString(). The two-arg constructor sets RetryAfterSeconds for the lockout / rate-limit paths.

Dependencies

  • EnumExtensions — for GetDescriptions<T>()

Consumers

  • BusinessExceptionHandler — catches and maps via MapStatusCode. The default mapping is 409; cycle 2 codes use a per-enum status map (AccountLocked → 423, LoginRateLimited → 429, refresh/MFA validation failures → 401, SessionNotFound → 404, mission validation failures → 400, MFA conflict states → 409). When RetryAfterSeconds > 0 the handler also stamps a Retry-After response header.
  • UserService — throws for the auth path (NoEmailFound, WrongPassword, EmailExists, UserDisabled, AccountLocked, LoginRateLimited)
  • RefreshTokenService — throws InvalidRefreshToken on bad/expired/reuse-detected
  • SessionService — throws SessionNotFound for admin-revoke of missing sids
  • MissionTokenService — throws InvalidMissionRequest, AircraftNotFound
  • MfaService — throws MfaAlreadyEnabled, MfaNotEnrolling, MfaNotEnabled, InvalidMfaCode, InvalidMfaToken, NoEmailFound, WrongPassword
  • ResourcesService — throws NoFileProvided for missing file uploads
  • Program.cs ParseSidClaim / ParseUserIdClaim helpers — throw InvalidRefreshToken (401) on missing or malformed claims
  • FluentValidation validators — reference ExceptionEnum codes in .WithErrorCode()

Data Models

None.

Configuration

None.

External Integrations

None.

Security

Error codes are returned to the client via BusinessExceptionHandler along with the per-enum HTTP status. The Retry-After header on lockout / rate-limit responses lets well-behaved clients back off without blind retries.

Tests

None.