10 -- Error Handling: Admin Service
This document defines the error handling strategy for the Admin Service feature, covering business error codes, exception hierarchy, error flow diagrams, global filter behavior, retry strategies, and the isBlocked check integration across all authentication strategies.
1. Business Error Registry
Заголовок раздела «1. Business Error Registry»Every business error thrown by the Admin Service maps to a custom error code, an HTTP status, a human-readable message, the service that throws it, and the condition that triggers it.
| # | Error Code | HTTP Status | Message | Thrown By | When |
|---|---|---|---|---|---|
| 1 | ADMIN_DOMAIN_NOT_ALLOWED | 400 | Email domain not allowed | AdminAuthService | Registration or login with an email whose domain is not in ADMIN_ALLOWED_DOMAINS |
| 2 | ADMIN_USER_NOT_FOUND | 404 | User not found | AdminAuthService | Login attempt with an email that does not exist in the database |
| 3 | ADMIN_NOT_ADMIN_ROLE | 403 | User is not an admin | AdminAuthService | Login attempt by a user who exists but does not have the ADMIN role |
| 4 | ADMIN_USER_BLOCKED | 403 | User is blocked | AdminAuthService | Login attempt by a user whose isBlocked flag is true |
| 5 | ADMIN_CODE_INVALID | 400 | Invalid or expired verification code | AdminAuthService | Verification code does not match or has expired in Redis |
| 6 | ADMIN_CODE_COOLDOWN | 429 | Please wait before requesting a new code | AdminAuthService | Code requested before the cooldown period (TTL) has elapsed |
| 7 | ADMIN_REGISTER_EXISTS | 409 | User already exists | PrismaExceptionFilter | Registration with an email that already exists (Prisma P2002 on email unique constraint) |
| 8 | ADMIN_JWT_INVALID | 401 | Unauthorized | AdminAccessTokenGuard | Bearer token is missing, malformed, or signature verification fails |
| 9 | ADMIN_JWT_EXPIRED | 401 | Unauthorized | AdminAccessTokenGuard | Bearer token has passed its exp claim |
| 10 | ADMIN_REFRESH_INVALID | 401 | Unauthorized | AdminRefreshTokenGuard | Refresh token is missing, invalid, or revoked |
| 11 | ADMIN_PROJECT_NOT_FOUND | 404 | Project not found | AdminProjectService | Project ID does not exist or has been soft-deleted (deletedAt IS NOT NULL) |
| 12 | ADMIN_INVALID_STATUS_TRANSITION | 400 | Cannot transition from X to Y | AdminProjectService | Requested status change violates the allowed transition rules |
| 13 | ADMIN_CANNOT_BLOCK_ADMIN | 403 | Cannot block admin user | AdminUserService | Attempt to block a user who has the ADMIN role |
| 14 | ADMIN_USER_ALREADY_BLOCKED | 400 | User is already blocked | AdminUserService | Block request for a user whose isBlocked is already true |
| 15 | ADMIN_USER_NOT_BLOCKED | 400 | User is not blocked | AdminUserService | Unblock request for a user whose isBlocked is already false |
| 16 | ADMIN_TARGET_USER_NOT_FOUND | 404 | User not found | AdminUserService | Block/unblock/detail request for a user ID that does not exist |
| 17 | ADMIN_VALIDATION_ERROR | 400 | [array of constraint violations] | ValidationPipe | DTO validation fails (missing fields, wrong types, constraint violations) |
2. Exception Class Hierarchy
Заголовок раздела «2. Exception Class Hierarchy»The Admin Service uses built-in NestJS exception classes exclusively. No custom exception subclasses are introduced. The diagram below shows how business errors map to existing exception types.
classDiagram class HttpException { +number status +string message +getStatus() number +getResponse() object }
class BadRequestException { +number status = 400 }
class UnauthorizedException { +number status = 401 }
class ForbiddenException { +number status = 403 }
class NotFoundException { +number status = 404 }
HttpException <|-- BadRequestException HttpException <|-- UnauthorizedException HttpException <|-- ForbiddenException HttpException <|-- NotFoundException
BadRequestException .. ADMIN_DOMAIN_NOT_ALLOWED BadRequestException .. ADMIN_CODE_INVALID BadRequestException .. ADMIN_CODE_COOLDOWN BadRequestException .. ADMIN_INVALID_STATUS_TRANSITION BadRequestException .. ADMIN_USER_ALREADY_BLOCKED BadRequestException .. ADMIN_USER_NOT_BLOCKED
UnauthorizedException .. ADMIN_JWT_INVALID UnauthorizedException .. ADMIN_JWT_EXPIRED UnauthorizedException .. ADMIN_REFRESH_INVALID
ForbiddenException .. ADMIN_NOT_ADMIN_ROLE ForbiddenException .. ADMIN_USER_BLOCKED ForbiddenException .. ADMIN_CANNOT_BLOCK_ADMIN
NotFoundException .. ADMIN_USER_NOT_FOUND NotFoundException .. ADMIN_PROJECT_NOT_FOUND NotFoundException .. ADMIN_TARGET_USER_NOT_FOUNDNote:
ADMIN_CODE_COOLDOWNusesBadRequestExceptionwith a 400 status. If rate-limiting middleware is added in the future, a 429 response could be returned at the HTTP layer instead.
3. Error Flow Diagrams
Заголовок раздела «3. Error Flow Diagrams»3.1 Admin Registration Error Flow
Заголовок раздела «3.1 Admin Registration Error Flow»flowchart TD A([POST /v1/admin/auth/register]) --> B{Email domain allowed?} B -- No --> C[400 BadRequestException: Email domain not allowed] B -- Yes --> D{User exists in DB?} D -- Yes, verified --> E[409 Conflict: Prisma P2002] D -- Yes, unverified --> F{Cooldown expired?} D -- No --> G[Create user record] G --> F F -- No --> H[400 BadRequestException: Please wait before requesting a new code] F -- Yes --> I[Generate 6-digit code] I --> J[Store code in Redis with TTL] J --> K[Send verification email] K -- Failure --> L[Retry with exponential backoff up to 3 attempts] L -- All retries failed --> M[500 Internal Server Error] K -- Success --> N([201 Response: message sent]) L -- Retry succeeds --> N3.2 Admin Login Error Flow
Заголовок раздела «3.2 Admin Login Error Flow»flowchart TD A([POST /v1/admin/auth/login]) --> B{Email domain allowed?} B -- No --> C[400 BadRequestException: Email domain not allowed] B -- Yes --> D{User exists?} D -- No --> E[404 NotFoundException: User not found] D -- Yes --> F{Has ADMIN role?} F -- No --> G[403 ForbiddenException: User is not an admin] F -- Yes --> H{isBlocked?} H -- Yes --> I[403 ForbiddenException: User is blocked] H -- No --> J{Cooldown expired?} J -- No --> K[400 BadRequestException: Please wait before requesting a new code] J -- Yes --> L[Generate 6-digit code] L --> M[Store code in Redis with TTL] M --> N[Send verification email] N --> O([200 Response: message sent])3.3 Project Update with Status Transition Validation
Заголовок раздела «3.3 Project Update with Status Transition Validation»flowchart TD A([PATCH /v1/admin/projects/:id]) --> B{JWT valid?} B -- No --> C[401 UnauthorizedException] B -- Yes --> D{Project exists and not soft-deleted?} D -- No --> E[404 NotFoundException: Project not found] D -- Yes --> F{Status field in request body?} F -- No --> G[Apply other field updates] F -- Yes --> H{Is transition from currentStatus to newStatus valid?} H -- No --> I[400 BadRequestException: Cannot transition from X to Y] H -- Yes --> G G --> J[Save updated project] J --> K([200 Response: AdminProjectResponseDto])3.4 User Block Error Flow
Заголовок раздела «3.4 User Block Error Flow»flowchart TD A([PATCH /v1/admin/users/:id/block]) --> B{JWT valid?} B -- No --> C[401 UnauthorizedException] B -- Yes --> D{Target user exists?} D -- No --> E[404 NotFoundException: User not found] D -- Yes --> F{Target user has ADMIN role?} F -- Yes --> G[403 ForbiddenException: Cannot block admin user] F -- No --> H{User already blocked?} H -- Yes --> I[400 BadRequestException: User is already blocked] H -- No --> J[Set isBlocked = true] J --> K([200 Response: message])4. Global Exception Filter Behavior
Заголовок раздела «4. Global Exception Filter Behavior»All apps in the monorepo register PrismaExceptionFilter globally alongside the NestJS default exception handler. The table below describes the complete exception-to-response mapping.
| # | Exception Type | HTTP Status | Response Format | Handler |
|---|---|---|---|---|
| 1 | PrismaClientKnownRequestError (code P2002) | 409 Conflict | { statusCode: 409, message: string, meta: object } | PrismaExceptionFilter |
| 2 | PrismaClientKnownRequestError (code P2025) | 400 Bad Request | { statusCode: 400, message: string, meta: object } | PrismaExceptionFilter (falls to default branch) |
| 3 | PrismaClientValidationError | 400 Bad Request | { statusCode: 400, message: string, meta: object } | PrismaExceptionFilter (caught by @Catch decorator) |
| 4 | PrismaClientKnownRequestError (other codes) | 400 Bad Request | { statusCode: 400, message: string, meta: object } | PrismaExceptionFilter (default branch) |
| 5 | BadRequestException | 400 Bad Request | { statusCode: 400, message: string | string[], error: "Bad Request" } | NestJS built-in handler |
| 6 | UnauthorizedException | 401 Unauthorized | { statusCode: 401, message: string, error: "Unauthorized" } | NestJS built-in handler |
| 7 | ForbiddenException | 403 Forbidden | { statusCode: 403, message: string, error: "Forbidden" } | NestJS built-in handler |
| 8 | NotFoundException | 404 Not Found | { statusCode: 404, message: string, error: "Not Found" } | NestJS built-in handler |
| 9 | ConflictException | 409 Conflict | { statusCode: 409, message: string, error: "Conflict" } | NestJS built-in handler |
| 10 | ValidationPipe rejection | 400 Bad Request | { statusCode: 400, message: string[], error: "Bad Request" } | NestJS ValidationPipe (global) |
| 11 | Unknown / unhandled exceptions | 500 Internal Server Error | { statusCode: 500, message: "Internal server error" } | BaseExceptionFilter (parent of PrismaExceptionFilter) |
Note on P2025: The current
PrismaExceptionFilterdoes not explicitly handle P2025. It falls through to the default 400 branch. Admin Service code throwsNotFoundExceptionexplicitly before Prisma reaches P2025 for not-found cases, so the filter’s default branch is acceptable.
5. Retry Strategy
Заголовок раздела «5. Retry Strategy»| # | Operation | Retryable? | Strategy | Max Attempts | Backoff | Notes |
|---|---|---|---|---|---|---|
| 1 | Email sending (verification code) | Yes | Exponential backoff | 3 | 1s, 2s, 4s | Failure after all retries results in 500; code remains in Redis for manual retry by user |
| 2 | Redis operations (code store/read) | Yes | Immediate retry | 2 | None | Redis is used for verification codes and cooldown; brief network blips are retryable |
| 3 | Database queries (Prisma) | No | Fail fast | 1 | N/A | Prisma manages its own connection pool; application-level retry risks duplicate writes |
| 4 | JWT generation (sign) | No | Fail fast | 1 | N/A | CPU-bound operation; failure indicates a configuration error, not a transient issue |
| 5 | JWT verification (verify) | No | Fail fast | 1 | N/A | Deterministic operation; retrying an invalid token will never succeed |
| 6 | Password hashing (bcrypt) | No | Fail fast | 1 | N/A | CPU-bound; failure is not transient |
6. isBlocked Check Integration
Заголовок раздела «6. isBlocked Check Integration»The Admin Service introduces an isBlocked boolean field on the User model. When a user is blocked by an admin, all existing authentication strategies must reject that user’s tokens on subsequent requests. This is implemented by adding an isBlocked check to the validate() method of each access-token strategy.
6.1 Strategy Modification Pattern
Заголовок раздела «6.1 Strategy Modification Pattern»Each strategy’s validate() method currently finds the token record and returns the payload. The modification adds a user lookup and blocked check between token validation and payload return.
Before (existing pattern):
async validate(payload: TokenDecodePayload) { const token = await this.tokenService.findOneByAccessTokenId(payload.jti, payload.userId); if (!token) { throw new UnauthorizedException(); } return payload;}After (with isBlocked check):
async validate(payload: TokenDecodePayload) { const token = await this.tokenService.findOneByAccessTokenId(payload.jti, payload.userId); if (!token) { throw new UnauthorizedException(); }
const user = await this.userService.findOneById(payload.userId); if (!user || user.isBlocked) { throw new UnauthorizedException('Account is blocked'); }
return payload;}6.2 Affected Strategies
Заголовок раздела «6.2 Affected Strategies»| # | Strategy | File | Guard | Behavior |
|---|---|---|---|---|
| 1 | CustomerAccessTokenStrategy | libs/apis/providers/auth-api/features/customer-auth/src/lib/strategies/customer-access-token.strategy.ts | CustomerAccessTokenGuard | After token lookup, fetch user and check isBlocked. Throw UnauthorizedException("Account is blocked") if true. |
| 2 | EmployeeAccessTokenStrategy | libs/apis/providers/auth-api/features/employee-auth/src/lib/strategies/employee-access-token.strategy.ts | EmployeeAccessTokenGuard | Same pattern as Customer. |
| 3 | AdminAccessTokenStrategy | libs/apis/providers/auth-api/features/admin-auth/src/lib/strategies/admin-access-token.strategy.ts | AdminAccessTokenGuard | Same pattern. Defensive only — the spec prevents admins from blocking other admins (ADMIN_CANNOT_BLOCK_ADMIN), so an admin should never have isBlocked = true. The check is included as a safety net against direct database manipulation. |
6.3 Dependency Injection Change
Заголовок раздела «6.3 Dependency Injection Change»Each strategy must inject UserService (or the relevant service that exposes findOneById) in addition to its existing dependencies. This requires the corresponding auth module to import UserCoreModule if it does not already do so.
- CustomerAuthModule — already imports
UserCoreModule(no change needed). - EmployeeAuthModule — must add
UserCoreModuleimport. - AdminAuthModule — already imports
UserCoreModuleby design (no change needed).
6.4 Refresh Token Strategies
Заголовок раздела «6.4 Refresh Token Strategies»Refresh token strategies (CustomerRefreshTokenStrategy, EmployeeRefreshTokenStrategy, AdminRefreshTokenStrategy) should also include the isBlocked check to prevent blocked users from obtaining new access tokens via refresh.
7. Error Response Format
Заголовок раздела «7. Error Response Format»All error responses across the Admin Service follow the standard NestJS error envelope:
{ "statusCode": 400, "message": "Email domain not allowed", "error": "Bad Request"}For validation errors (from ValidationPipe), the message field is an array:
{ "statusCode": 400, "message": [ "email must be an email", "email should not be empty" ], "error": "Bad Request"}For Prisma errors (from PrismaExceptionFilter), the response includes a meta field:
{ "statusCode": 409, "message": "Unique constraint failed on the fields: (`email`)", "meta": { "target": ["email"] }}