05 -- API Contracts: Admin Service
1. Endpoint Table
Заголовок раздела «1. Endpoint Table»| # | Method | Path | Guard | Roles | Request DTO | Response DTO | Success | Error Codes |
|---|---|---|---|---|---|---|---|---|
| 1 | POST | /v1/admin/auth/register | None | None | AdminRegisterDto | { message: string } | 201 | 400, 409 |
| 2 | POST | /v1/admin/auth/register/verify | None | None | AdminRegisterVerifyDto | AdminAuthResponseDto | 201 | 400, 404, 429 |
| 3 | POST | /v1/admin/auth/login | None | None | AdminLoginDto | { message: string } | 200 | 400, 404 |
| 4 | POST | /v1/admin/auth/login/verify | None | None | AdminLoginVerifyDto | AdminAuthResponseDto | 200 | 400, 404, 429 |
| 5 | POST | /v1/admin/auth/refresh | AdminRefreshTokenGuard | ADMIN | Headers only | { token: string } | 200 | 401 |
| 6 | GET | /v1/admin/projects | AdminAccessTokenGuard | ADMIN | AdminProjectListQueryDto (query) | PaginatedResponse<AdminProjectResponseDto> | 200 | 401 |
| 7 | GET | /v1/admin/projects/:id | AdminAccessTokenGuard | ADMIN | Path: id (UUID) | AdminProjectResponseDto | 200 | 401, 404 |
| 8 | PATCH | /v1/admin/projects/:id | AdminAccessTokenGuard | ADMIN | AdminProjectUpdateDto | AdminProjectResponseDto | 200 | 400, 401, 404 |
| 9 | DELETE | /v1/admin/projects/:id | AdminAccessTokenGuard | ADMIN | Path: id (UUID) | None | 204 | 401, 404 |
| 10 | GET | /v1/admin/users | AdminAccessTokenGuard | ADMIN | AdminUserListQueryDto (query) | PaginatedResponse<AdminUserResponseDto> | 200 | 401 |
| 11 | GET | /v1/admin/users/:id | AdminAccessTokenGuard | ADMIN | Path: id (UUID) | AdminUserDetailResponseDto | 200 | 401, 404 |
| 12 | PATCH | /v1/admin/users/:id/block | AdminAccessTokenGuard | ADMIN | Path: id (UUID) | { message: string } | 200 | 401, 403, 404 |
| 13 | PATCH | /v1/admin/users/:id/unblock | AdminAccessTokenGuard | ADMIN | Path: id (UUID) | { message: string } | 200 | 401, 404 |
2. DTO Class Diagrams
Заголовок раздела «2. DTO Class Diagrams»2.1 Admin Auth DTOs
Заголовок раздела «2.1 Admin Auth DTOs»classDiagram class AdminRegisterDto { +string email %% @IsEmail() %% @Trim() %% Validated against ADMIN_ALLOWED_DOMAINS }
class AdminRegisterVerifyDto { +string email +string code +string fingerprint %% @IsEmail() %% @IsString() @MinLength(6) @MaxLength(6) %% @IsString() }
class AdminLoginDto { +string email %% @IsEmail() %% @Trim() }
class AdminLoginVerifyDto { +string email +string code +string fingerprint %% @IsEmail() %% @IsString() @MinLength(6) @MaxLength(6) %% @IsString() }
class AdminAuthResponseDto { +string accessToken +string refreshToken %% @Expose() %% Set-Cookie header also sent }
class AdminRefreshResponseDto { +string token %% @Expose() }
AdminRegisterVerifyDto --> AdminAuthResponseDto : returns AdminLoginVerifyDto --> AdminAuthResponseDto : returns2.2 Admin Project DTOs
Заголовок раздела «2.2 Admin Project DTOs»classDiagram class BaseDto { +string id +Date createdAt +Date updatedAt %% @Expose() @IsUUID() %% @Expose() @IsDate() }
class AdminProjectOwnerDto { +string id +string email %% @Expose() @IsUUID() %% @Expose() @IsEmail() }
class AdminProjectResponseDto { +string id +string title +string description +ProjectCategoryType? category +ProjectStatus status +string customerId +Date createdAt +Date updatedAt +AdminProjectOwnerDto owner %% @Expose() for all fields %% @ValidateNested() @Type(() => AdminProjectOwnerDto) }
class AdminProjectUpdateDto { +string? title +string? description +ProjectCategoryType? category +ProjectStatus? status %% All fields @IsOptional() %% title: @IsString() @MinLength(3) @MaxLength(255) @Trim() %% description: @IsString() @MinLength(3) @Trim() %% category: @IsEnum(ProjectCategoryType) %% status: @IsEnum(ProjectStatus) validated against PROJECT_STATUS_TRANSITIONS }
class AdminProjectListQueryDto { +number? page +number? perPage +ProjectStatus? status +ProjectCategoryType? category +string? search %% page: @IsOptional() @IsInt() @Min(1) default 1 %% perPage: @IsOptional() @IsInt() @Min(1) default 10 %% status: @IsOptional() @IsEnum(ProjectStatus) %% category: @IsOptional() @IsEnum(ProjectCategoryType) %% search: @IsOptional() @IsString() @Trim() }
BaseDto <|-- AdminProjectResponseDto AdminProjectResponseDto *-- AdminProjectOwnerDto : owner2.3 Admin User DTOs
Заголовок раздела «2.3 Admin User DTOs»classDiagram class AdminUserRoleDto { +string roleType %% @Expose() %% RoleType: CUSTOMER | EMPLOYEE | ADMIN }
class AdminUserResponseDto { +string id +string email +string? phone +boolean isEmailVerified +boolean isBlocked +Date createdAt +Date updatedAt +AdminUserRoleDto[] roles %% @Expose() for all fields %% @ValidateNested(each: true) @Type(() => AdminUserRoleDto) }
class AdminUserDetailResponseDto { +string id +string email +string? phone +boolean isEmailVerified +boolean isBlocked +Date createdAt +Date updatedAt +AdminUserRoleDto[] roles +AdminProjectResponseDto[] projects %% Extends AdminUserResponseDto with projects }
class AdminUserListQueryDto { +number? page +number? perPage +string? role +boolean? isBlocked +string? search %% page: @IsOptional() @IsInt() @Min(1) default 1 %% perPage: @IsOptional() @IsInt() @Min(1) default 10 %% role: @IsOptional() @IsEnum(RoleType) %% isBlocked: @IsOptional() @IsBoolean() %% search: @IsOptional() @IsString() @Trim() (searches by email) }
AdminUserResponseDto *-- AdminUserRoleDto : roles AdminUserResponseDto <|-- AdminUserDetailResponseDto AdminUserDetailResponseDto *-- AdminProjectResponseDto : projects
class AdminProjectResponseDto { +string id +string title +ProjectStatus status +Date createdAt ... }3. Project Status State Machine
Заголовок раздела «3. Project Status State Machine»stateDiagram-v2 [*] --> DRAFT
DRAFT --> PUBLISHED : Customer: publish\nAdmin: PATCH /admin/projects/:id
PUBLISHED --> HIRING : Customer: start hiring\nAdmin: PATCH /admin/projects/:id PUBLISHED --> CANCELLED : Customer: cancel\nAdmin: PATCH /admin/projects/:id
HIRING --> IN_PROGRESS : Customer: start work\nAdmin: PATCH /admin/projects/:id HIRING --> CANCELLED : Customer: cancel\nAdmin: PATCH /admin/projects/:id
IN_PROGRESS --> REVIEW : Customer: submit for review\nAdmin: PATCH /admin/projects/:id IN_PROGRESS --> CANCELLED : Customer: cancel\nAdmin: PATCH /admin/projects/:id IN_PROGRESS --> DISPUTED : Customer: raise dispute\nAdmin: PATCH /admin/projects/:id
REVIEW --> COMPLETED : Customer: approve\nAdmin: PATCH /admin/projects/:id REVIEW --> IN_PROGRESS : Customer: request changes\nAdmin: PATCH /admin/projects/:id
DISPUTED --> CANCELLED : Admin: PATCH /admin/projects/:id\nAdmin: resolve dispute DISPUTED --> IN_PROGRESS : Admin: PATCH /admin/projects/:id\nAdmin: resolve dispute
COMPLETED --> [*] CANCELLED --> [*]Transition rules enforced at the service layer:
| Current Status | Allowed Transitions |
|---|---|
DRAFT | PUBLISHED |
PUBLISHED | HIRING, CANCELLED |
HIRING | IN_PROGRESS, CANCELLED |
IN_PROGRESS | REVIEW, CANCELLED, DISPUTED |
REVIEW | COMPLETED, IN_PROGRESS |
DISPUTED | CANCELLED, IN_PROGRESS |
COMPLETED | (terminal — no transitions allowed) |
CANCELLED | (terminal — no transitions allowed) |
When an admin sends PATCH /v1/admin/projects/:id with a status field, the service validates the requested transition against PROJECT_STATUS_TRANSITIONS. If the transition is not in the allowed list, a 400 Bad Request is returned with the message "Invalid status transition from {current} to {requested}".
4. Request/Response Examples
Заголовок раздела «4. Request/Response Examples»4.1 POST /v1/admin/auth/register/verify
Заголовок раздела «4.1 POST /v1/admin/auth/register/verify»Request:
POST /v1/admin/auth/register/verify HTTP/1.1Content-Type: application/json
{ "email": "admin@crewsforge.com", "code": "482910", "fingerprint": "d7a8fbb307d7809469ca9abcb0082e4f"}Response (201 Created):
HTTP/1.1 201 CreatedSet-Cookie: refreshToken=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Secure; Path=/v1/admin/auth; SameSite=StrictContent-Type: application/json
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1NTBjOGEwMC0xYTJiLTRjM2QtOGU0Zi01NjZhN2I4OWMwZGUiLCJyb2xlcyI6WyJBRE1JTiJdLCJpYXQiOjE3MTExMDAwMDAsImV4cCI6MTcxMTEwMzYwMH0.abc123", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1NTBjOGEwMC0xYTJiLTRjM2QtOGU0Zi01NjZhN2I4OWMwZGUiLCJpYXQiOjE3MTExMDAwMDAsImV4cCI6MTcxMTcwNDgwMH0.def456"}Token payload (decoded accessToken):
{ "userId": "550c8a00-1a2b-4c3d-8e4f-566a7b89c0de", "roles": ["ADMIN"], "iat": 1711100000, "exp": 1711103600}Error (400 — invalid/expired code):
{ "statusCode": 400, "message": "Verification code is invalid or expired", "error": "Bad Request"}4.2 GET /v1/admin/projects
Заголовок раздела «4.2 GET /v1/admin/projects»Request:
GET /v1/admin/projects?page=1&perPage=2&status=IN_PROGRESS&search=website HTTP/1.1Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Response (200 OK):
{ "data": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "title": "Corporate Website Redesign", "description": "Full redesign of the corporate website with new branding guidelines...", "category": "WEB_DEVELOPMENT", "status": "IN_PROGRESS", "customerId": "c1d2e3f4-a5b6-7890-cdef-123456789abc", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-03-10T14:22:00.000Z", "owner": { "id": "c1d2e3f4-a5b6-7890-cdef-123456789abc", "email": "client@example.com" } }, { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "title": "Website Performance Optimization", "description": "Optimize page load times and Core Web Vitals for the main website...", "category": "WEB_DEVELOPMENT", "status": "IN_PROGRESS", "customerId": "d2e3f4a5-b6c7-8901-defg-234567890bcd", "createdAt": "2026-02-01T08:00:00.000Z", "updatedAt": "2026-03-18T16:45:00.000Z", "owner": { "id": "d2e3f4a5-b6c7-8901-defg-234567890bcd", "email": "another-client@example.com" } } ], "meta": { "totalCount": 7, "pageCount": 4, "currentPage": 1, "perPage": 2, "isFirstPage": true, "isLastPage": false }}4.3 PATCH /v1/admin/users/:id/block
Заголовок раздела «4.3 PATCH /v1/admin/users/:id/block»Request:
PATCH /v1/admin/users/550c8a00-1a2b-4c3d-8e4f-566a7b89c0de/block HTTP/1.1Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Response (200 OK — blocking a customer/employee):
{ "message": "User has been blocked successfully"}Error (403 Forbidden — attempting to block an admin):
{ "statusCode": 403, "message": "Cannot block a user with ADMIN role", "error": "Forbidden"}Error (404 Not Found — user does not exist):
{ "statusCode": 404, "message": "User not found", "error": "Not Found"}5. Error Responses
Заголовок раздела «5. Error Responses»5.1 Common Error Format
Заголовок раздела «5.1 Common Error Format»All errors follow the standard NestJS HttpException shape:
{ "statusCode": <number>, "message": "<string | string[]>", "error": "<HTTP status text>"}5.2 Error Codes Table
Заголовок раздела «5.2 Error Codes Table»| Status | Error | Endpoints | Condition |
|---|---|---|---|
| 400 | Bad Request | POST /admin/auth/register | Email domain not in ADMIN_ALLOWED_DOMAINS |
| 400 | Bad Request | POST /admin/auth/register/verify, POST /admin/auth/login/verify | Verification code is invalid or expired |
| 400 | Bad Request | PATCH /admin/projects/:id | Invalid status transition (e.g., COMPLETED to DRAFT) |
| 400 | Bad Request | Any with body/query | DTO validation failure (class-validator) — message is an array of constraint violations |
| 401 | Unauthorized | All guarded endpoints | Missing, malformed, or expired access/refresh token |
| 401 | Unauthorized | All guarded endpoints | User associated with token has isBlocked = true |
| 403 | Forbidden | PATCH /admin/users/:id/block | Target user has the ADMIN role |
| 404 | Not Found | POST /admin/auth/login | No user found with given email and ADMIN role |
| 404 | Not Found | GET /admin/projects/:id, PATCH /admin/projects/:id, DELETE /admin/projects/:id | Project not found or already soft-deleted (deletedAt IS NOT NULL) |
| 404 | Not Found | GET /admin/users/:id, PATCH /admin/users/:id/block, PATCH /admin/users/:id/unblock | User not found |
| 409 | Conflict | POST /admin/auth/register | User with this email already exists |
| 429 | Too Many Requests | POST /admin/auth/register, POST /admin/auth/login | Verification code resend cooldown has not expired (120s) |
5.3 Validation Error Example (400)
Заголовок раздела «5.3 Validation Error Example (400)»When class-validator rejects a request body, the response contains an array of human-readable constraint messages:
{ "statusCode": 400, "message": [ "title must be longer than or equal to 3 characters", "status must be one of the following values: DRAFT, PUBLISHED, HIRING, IN_PROGRESS, REVIEW, COMPLETED, CANCELLED, DISPUTED" ], "error": "Bad Request"}6. Headers and Authentication
Заголовок раздела «6. Headers and Authentication»6.1 Request Headers for Guarded Endpoints
Заголовок раздела «6.1 Request Headers for Guarded Endpoints»| Header | Required | Description | Used By |
|---|---|---|---|
Authorization | Yes | Bearer <accessToken> or Bearer <refreshToken> | All guarded endpoints |
X-Fingerprint | Yes (refresh only) | Client fingerprint string | POST /admin/auth/refresh |
X-Session-Id | Yes (refresh only) | Active session UUID | POST /admin/auth/refresh |
6.2 Decorators Used in Controllers
Заголовок раздела «6.2 Decorators Used in Controllers»| Decorator | Source | Purpose |
|---|---|---|
@GetTokenPayload() | libs/apis/shared | Extracts decoded JWT payload from request |
@HeaderFingerprint() | libs/apis/shared | Extracts X-Fingerprint header value |
@HeaderSessionId() | libs/apis/shared | Extracts X-Session-Id header value |
@UseGuards(AdminAccessTokenGuard) | @nestjs/common + shared | Protects admin CRUD endpoints |
@UseGuards(AdminRefreshTokenGuard) | @nestjs/common + shared | Protects the refresh endpoint |
7. Pagination Contract
Заголовок раздела «7. Pagination Contract»All list endpoints (GET /admin/projects, GET /admin/users) use the shared pagination contract powered by MapperService.toPaginateResponse().
Query Parameters
Заголовок раздела «Query Parameters»| Parameter | Type | Default | Constraints |
|---|---|---|---|
page | number | 1 | @IsInt() @Min(1) |
perPage | number | 10 | @IsInt() @Min(1) |
Response Wrapper
Заголовок раздела «Response Wrapper»interface PaginatedResponse<T> { data: T[]; meta: { totalCount: number; // Total records matching filters pageCount: number; // Total pages (ceil(totalCount / perPage)) currentPage: number; // Current page number perPage: number; // Items per page isFirstPage: boolean; // currentPage === 1 isLastPage: boolean; // currentPage === pageCount };}