Перейти к содержимому

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.


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 CodeHTTP StatusMessageThrown ByWhen
1ADMIN_DOMAIN_NOT_ALLOWED400Email domain not allowedAdminAuthServiceRegistration or login with an email whose domain is not in ADMIN_ALLOWED_DOMAINS
2ADMIN_USER_NOT_FOUND404User not foundAdminAuthServiceLogin attempt with an email that does not exist in the database
3ADMIN_NOT_ADMIN_ROLE403User is not an adminAdminAuthServiceLogin attempt by a user who exists but does not have the ADMIN role
4ADMIN_USER_BLOCKED403User is blockedAdminAuthServiceLogin attempt by a user whose isBlocked flag is true
5ADMIN_CODE_INVALID400Invalid or expired verification codeAdminAuthServiceVerification code does not match or has expired in Redis
6ADMIN_CODE_COOLDOWN429Please wait before requesting a new codeAdminAuthServiceCode requested before the cooldown period (TTL) has elapsed
7ADMIN_REGISTER_EXISTS409User already existsPrismaExceptionFilterRegistration with an email that already exists (Prisma P2002 on email unique constraint)
8ADMIN_JWT_INVALID401UnauthorizedAdminAccessTokenGuardBearer token is missing, malformed, or signature verification fails
9ADMIN_JWT_EXPIRED401UnauthorizedAdminAccessTokenGuardBearer token has passed its exp claim
10ADMIN_REFRESH_INVALID401UnauthorizedAdminRefreshTokenGuardRefresh token is missing, invalid, or revoked
11ADMIN_PROJECT_NOT_FOUND404Project not foundAdminProjectServiceProject ID does not exist or has been soft-deleted (deletedAt IS NOT NULL)
12ADMIN_INVALID_STATUS_TRANSITION400Cannot transition from X to YAdminProjectServiceRequested status change violates the allowed transition rules
13ADMIN_CANNOT_BLOCK_ADMIN403Cannot block admin userAdminUserServiceAttempt to block a user who has the ADMIN role
14ADMIN_USER_ALREADY_BLOCKED400User is already blockedAdminUserServiceBlock request for a user whose isBlocked is already true
15ADMIN_USER_NOT_BLOCKED400User is not blockedAdminUserServiceUnblock request for a user whose isBlocked is already false
16ADMIN_TARGET_USER_NOT_FOUND404User not foundAdminUserServiceBlock/unblock/detail request for a user ID that does not exist
17ADMIN_VALIDATION_ERROR400[array of constraint violations]ValidationPipeDTO validation fails (missing fields, wrong types, constraint violations)

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_FOUND

Note: ADMIN_CODE_COOLDOWN uses BadRequestException with a 400 status. If rate-limiting middleware is added in the future, a 429 response could be returned at the HTTP layer instead.


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 --> N
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])
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])
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])

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 TypeHTTP StatusResponse FormatHandler
1PrismaClientKnownRequestError (code P2002)409 Conflict{ statusCode: 409, message: string, meta: object }PrismaExceptionFilter
2PrismaClientKnownRequestError (code P2025)400 Bad Request{ statusCode: 400, message: string, meta: object }PrismaExceptionFilter (falls to default branch)
3PrismaClientValidationError400 Bad Request{ statusCode: 400, message: string, meta: object }PrismaExceptionFilter (caught by @Catch decorator)
4PrismaClientKnownRequestError (other codes)400 Bad Request{ statusCode: 400, message: string, meta: object }PrismaExceptionFilter (default branch)
5BadRequestException400 Bad Request{ statusCode: 400, message: string | string[], error: "Bad Request" }NestJS built-in handler
6UnauthorizedException401 Unauthorized{ statusCode: 401, message: string, error: "Unauthorized" }NestJS built-in handler
7ForbiddenException403 Forbidden{ statusCode: 403, message: string, error: "Forbidden" }NestJS built-in handler
8NotFoundException404 Not Found{ statusCode: 404, message: string, error: "Not Found" }NestJS built-in handler
9ConflictException409 Conflict{ statusCode: 409, message: string, error: "Conflict" }NestJS built-in handler
10ValidationPipe rejection400 Bad Request{ statusCode: 400, message: string[], error: "Bad Request" }NestJS ValidationPipe (global)
11Unknown / unhandled exceptions500 Internal Server Error{ statusCode: 500, message: "Internal server error" }BaseExceptionFilter (parent of PrismaExceptionFilter)

Note on P2025: The current PrismaExceptionFilter does not explicitly handle P2025. It falls through to the default 400 branch. Admin Service code throws NotFoundException explicitly before Prisma reaches P2025 for not-found cases, so the filter’s default branch is acceptable.


#OperationRetryable?StrategyMax AttemptsBackoffNotes
1Email sending (verification code)YesExponential backoff31s, 2s, 4sFailure after all retries results in 500; code remains in Redis for manual retry by user
2Redis operations (code store/read)YesImmediate retry2NoneRedis is used for verification codes and cooldown; brief network blips are retryable
3Database queries (Prisma)NoFail fast1N/APrisma manages its own connection pool; application-level retry risks duplicate writes
4JWT generation (sign)NoFail fast1N/ACPU-bound operation; failure indicates a configuration error, not a transient issue
5JWT verification (verify)NoFail fast1N/ADeterministic operation; retrying an invalid token will never succeed
6Password hashing (bcrypt)NoFail fast1N/ACPU-bound; failure is not transient

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.

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;
}
#StrategyFileGuardBehavior
1CustomerAccessTokenStrategylibs/apis/providers/auth-api/features/customer-auth/src/lib/strategies/customer-access-token.strategy.tsCustomerAccessTokenGuardAfter token lookup, fetch user and check isBlocked. Throw UnauthorizedException("Account is blocked") if true.
2EmployeeAccessTokenStrategylibs/apis/providers/auth-api/features/employee-auth/src/lib/strategies/employee-access-token.strategy.tsEmployeeAccessTokenGuardSame pattern as Customer.
3AdminAccessTokenStrategylibs/apis/providers/auth-api/features/admin-auth/src/lib/strategies/admin-access-token.strategy.tsAdminAccessTokenGuardSame 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.

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 UserCoreModule import.
  • AdminAuthModule — already imports UserCoreModule by design (no change needed).

Refresh token strategies (CustomerRefreshTokenStrategy, EmployeeRefreshTokenStrategy, AdminRefreshTokenStrategy) should also include the isBlocked check to prevent blocked users from obtaining new access tokens via refresh.


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"]
}
}