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

04 -- Data Flow: Admin Service

This document describes the data flow for every Admin Service endpoint using Mermaid sequence diagrams, and provides business logic flowcharts for the most complex service methods. It covers admin authentication (passwordless), project management, and user management.


Initiates passwordless registration by validating the admin email domain, checking for existing users and cooldowns, then generating and sending a verification code.

sequenceDiagram
autonumber
actor Client
participant Controller as AdminAuthController
participant Service as AdminAuthService
participant UserService as UserService
participant Redis as RedisClientService
participant Email as EmailSenderService
Client->>+Controller: POST /v1/admin/auth/register {email}
Controller->>+Service: register(dto)
Service->>Service: Validate email domain against ADMIN_ALLOWED_DOMAINS
alt Domain not allowed
Service-->>Controller: BadRequestException("Invalid email domain")
Controller-->>Client: 400 Bad Request
end
Service->>+UserService: findOneByEmail(email)
UserService-->>-Service: user | null
alt User already exists
Service-->>Controller: ConflictException("User already exists")
Controller-->>Client: 409 Conflict
end
Service->>+Redis: get("ADMIN_VERIFICATION_RESEND_CODE:email={email}")
Redis-->>-Service: cooldown | null
alt Cooldown active
Service-->>Controller: TooManyRequestsException("Please wait before resending")
Controller-->>Client: 429 Too Many Requests
end
Service->>Service: getRandomCode(6) -> code
Service->>+Redis: set("ADMIN_VERIFICATION_CODE:email={email}", code, TTL=900s)
Redis-->>-Service: OK
Service->>+Redis: set("ADMIN_VERIFICATION_RESEND_CODE:email={email}", "1", TTL=120s)
Redis-->>-Service: OK
Service->>+Email: sendAdminVerificationEmail(email, code)
Email-->>-Service: sent
Service-->>-Controller: { message: "Verification code sent" }
Controller-->>-Client: 201 { message: "Verification code sent" }

Verifies the registration code, creates the user with ADMIN role, establishes a session, and returns a JWT token pair.

sequenceDiagram
autonumber
actor Client
participant Controller as AdminAuthController
participant Service as AdminAuthService
participant Redis as RedisClientService
participant UserService as UserService
participant RoleService as UserOnRole
participant SessionService as SessionService
participant TokenAdmin as TokenAdminService
participant TokenService as TokenService
participant DB as PostgreSQL
Client->>+Controller: POST /v1/admin/auth/register/verify {email, code, fingerprint}
Controller->>+Service: registerVerify(dto)
Service->>+Redis: get("ADMIN_VERIFICATION_CODE:email={email}")
Redis-->>-Service: storedCode | null
alt Code not found (expired)
Service-->>Controller: NotFoundException("Verification code expired")
Controller-->>Client: 404 Not Found
end
alt Code does not match
Service-->>Controller: BadRequestException("Invalid verification code")
Controller-->>Client: 400 Bad Request
end
Service->>+Redis: del("ADMIN_VERIFICATION_CODE:email={email}")
Redis-->>-Service: OK
Note over Service,DB: @Transactional() begins
Service->>+UserService: create({email}) (no password)
UserService->>DB: INSERT INTO users
DB-->>UserService: user
UserService-->>-Service: user
Service->>+RoleService: create({userId, roleType: ADMIN})
RoleService->>DB: INSERT INTO user_on_role
DB-->>RoleService: userOnRole
RoleService-->>-Service: userOnRole
Service->>+SessionService: create({userId, fingerprint})
SessionService->>DB: INSERT INTO sessions
DB-->>SessionService: session
SessionService-->>-Service: session
Note over Service,DB: @Transactional() commits
Service->>+TokenAdmin: generateAdminTokenPair(payload)
TokenAdmin-->>-Service: {accessToken, refreshToken}
Service->>+TokenService: saveOne({sessionId, tokenId, ...})
TokenService->>DB: INSERT INTO tokens
DB-->>TokenService: token
TokenService-->>-Service: token
Service-->>-Controller: {accessToken, refreshToken}
Controller-->>-Client: 201 {accessToken, refreshToken} + Set-Cookie(refreshToken)

Initiates passwordless login by verifying admin role, checking the user is not blocked, then generating and sending a verification code.

sequenceDiagram
autonumber
actor Client
participant Controller as AdminAuthController
participant Service as AdminAuthService
participant UserService as UserService
participant Redis as RedisClientService
participant Email as EmailSenderService
Client->>+Controller: POST /v1/admin/auth/login {email}
Controller->>+Service: login(dto)
Service->>+UserService: findOneByEmail(email, include: userOnRole.role)
UserService-->>-Service: user | null
alt User not found
Service-->>Controller: NotFoundException("User not found")
Controller-->>Client: 404 Not Found
end
Service->>Service: Check user has ADMIN role
alt User does not have ADMIN role
Service-->>Controller: ForbiddenException("Not an admin")
Controller-->>Client: 403 Forbidden
end
Service->>Service: Check user.isBlocked === false
alt User is blocked
Service-->>Controller: ForbiddenException("User is blocked")
Controller-->>Client: 403 Forbidden
end
Service->>+Redis: get("ADMIN_VERIFICATION_RESEND_CODE:email={email}")
Redis-->>-Service: cooldown | null
alt Cooldown active
Service-->>Controller: TooManyRequestsException("Please wait before resending")
Controller-->>Client: 429 Too Many Requests
end
Service->>Service: getRandomCode(6) -> code
Service->>+Redis: set("ADMIN_VERIFICATION_CODE:email={email}", code, TTL=900s)
Redis-->>-Service: OK
Service->>+Redis: set("ADMIN_VERIFICATION_RESEND_CODE:email={email}", "1", TTL=120s)
Redis-->>-Service: OK
Service->>+Email: sendAdminVerificationEmail(email, code)
Email-->>-Service: sent
Service-->>-Controller: { message: "Verification code sent" }
Controller-->>-Client: 200 { message: "Verification code sent" }

Verifies the login code, finds or creates a session, and returns a JWT token pair.

sequenceDiagram
autonumber
actor Client
participant Controller as AdminAuthController
participant Service as AdminAuthService
participant Redis as RedisClientService
participant UserService as UserService
participant SessionService as SessionService
participant TokenAdmin as TokenAdminService
participant TokenService as TokenService
participant DB as PostgreSQL
Client->>+Controller: POST /v1/admin/auth/login/verify {email, code, fingerprint}
Controller->>+Service: loginVerify(dto)
Service->>+Redis: get("ADMIN_VERIFICATION_CODE:email={email}")
Redis-->>-Service: storedCode | null
alt Code not found (expired)
Service-->>Controller: NotFoundException("Verification code expired")
Controller-->>Client: 404 Not Found
end
alt Code does not match
Service-->>Controller: BadRequestException("Invalid verification code")
Controller-->>Client: 400 Bad Request
end
Service->>+Redis: del("ADMIN_VERIFICATION_CODE:email={email}")
Redis-->>-Service: OK
Service->>+UserService: findOneByEmail(email)
UserService-->>-Service: user
Service->>+SessionService: findOneBy({userId: user.id, fingerprint})
SessionService-->>-Service: session | null
alt Session not found
Service->>+SessionService: create({userId: user.id, fingerprint})
SessionService->>DB: INSERT INTO sessions
DB-->>SessionService: session
SessionService-->>-Service: session
end
Service->>+TokenAdmin: generateAdminTokenPair(payload)
TokenAdmin-->>-Service: {accessToken, refreshToken}
Service->>+TokenService: saveOne({sessionId, tokenId, ...})
TokenService->>DB: UPSERT INTO tokens
DB-->>TokenService: token
TokenService-->>-Service: token
Service->>+SessionService: update(session.id, {tokenId})
SessionService->>DB: UPDATE sessions
DB-->>SessionService: session
SessionService-->>-Service: session
Service-->>-Controller: {accessToken, refreshToken}
Controller-->>-Client: 200 {accessToken, refreshToken} + Set-Cookie(refreshToken)

Validates the refresh token via the AdminRefreshTokenGuard, then generates a new token pair and rotates the stored token.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminRefreshTokenGuard
participant Controller as AdminAuthRefreshTokenController
participant Service as AdminAuthRefreshTokenService
participant SessionService as SessionService
participant TokenAdmin as TokenAdminService
participant TokenService as TokenService
participant DB as PostgreSQL
Client->>+Guard: POST /v1/admin/auth/refresh (Cookie: refreshToken, Headers: fingerprint, sessionId)
Guard->>Guard: Validate refresh JWT with admin secret
alt Token invalid or expired
Guard-->>Client: 401 Unauthorized
else Token valid
Guard->>+Controller: Request (tokenPayload injected)
end
Controller->>Controller: @GetTokenPayload() -> tokenPayload
Controller->>Controller: @HeaderFingerprint() -> fingerprint
Controller->>Controller: @HeaderSessionId() -> sessionId
Controller->>+Service: refresh(tokenPayload, fingerprint, sessionId)
Service->>+SessionService: findOneById(sessionId)
SessionService-->>-Service: session | null
alt Session not found
Service-->>Controller: UnauthorizedException("Session not found")
Controller-->>Client: 401 Unauthorized
end
Service->>Service: Validate session.fingerprint === fingerprint
alt Fingerprint mismatch
Service-->>Controller: UnauthorizedException("Invalid fingerprint")
Controller-->>Client: 401 Unauthorized
end
Service->>+TokenAdmin: generateAdminTokenPair(newPayload)
TokenAdmin-->>-Service: {accessToken, refreshToken}
Service->>+TokenService: updateOne(tokenPayload.tokenId, {newTokenId, ...})
TokenService->>DB: UPDATE tokens
DB-->>TokenService: token
TokenService-->>-Service: token
Service-->>-Controller: {accessToken, refreshToken}
Controller-->>-Client: 200 {token: accessToken} + Set-Cookie(refreshToken)

Lists all non-deleted projects with optional filtering by status, category, and title search. Returns a paginated response.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminProjectController
participant Service as AdminProjectService
participant Repo as AdminProjectRepository
participant Mapper as MapperService
participant DB as PostgreSQL
Client->>+Guard: GET /v1/admin/projects?status=X&category=Y&title=Z&page=1&limit=10 (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request (user context injected)
end
Controller->>+Service: findAll(query)
Service->>Service: Build where clause
Note over Service: { deletedAt: null, ...(status), ...(category), ...(title: contains/insensitive) }
Service->>+Repo: findManyAndPaginate({ where, include: { customer: { include: { user: { select: {id, email} } } } }, paginate: getPaginate(query) })
Repo->>+DB: SELECT projects with joins
DB-->>-Repo: rows + count
Repo-->>-Service: paginatedResult
Service->>+Mapper: toPaginateResponse(result, AdminProjectResponseDto)
Mapper-->>-Service: paginatedResponse
Service-->>-Controller: paginatedResponse
Controller-->>-Client: 200 PaginatedResponse<AdminProjectResponseDto>

Retrieves a single non-deleted project by ID, including owner details.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminProjectController
participant Service as AdminProjectService
participant Repo as AdminProjectRepository
participant DB as PostgreSQL
Client->>+Guard: GET /v1/admin/projects/:id (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: findOneByIdOrFail(id)
Service->>+Repo: findOne({ where: { id, deletedAt: null }, include: { customer: { include: { user: true } } } })
Repo->>+DB: SELECT project WHERE id AND deletedAt IS NULL
DB-->>-Repo: project | null
Repo-->>-Service: project | null
alt Project not found
Service-->>Controller: NotFoundException("Project not found")
Controller-->>Client: 404 Not Found
else Project found
Service-->>-Controller: AdminProjectResponseDto
Controller-->>-Client: 200 AdminProjectResponseDto
end

Updates a project. If the status field is being changed, validates the transition against PROJECT_STATUS_TRANSITIONS.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminProjectController
participant Service as AdminProjectService
participant Repo as AdminProjectRepository
participant DB as PostgreSQL
Client->>+Guard: PATCH /v1/admin/projects/:id (Bearer accessToken) {status?, title?, ...}
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: updateOneById(id, dto)
Service->>+Repo: findOne({ where: { id, deletedAt: null } })
Repo->>+DB: SELECT project
DB-->>-Repo: project | null
Repo-->>-Service: project | null
alt Project not found
Service-->>Controller: NotFoundException("Project not found")
Controller-->>Client: 404 Not Found
end
alt dto contains status change
Service->>Service: Check PROJECT_STATUS_TRANSITIONS[currentStatus] includes newStatus
alt Invalid transition
Service-->>Controller: BadRequestException("Invalid status transition")
Controller-->>Client: 400 Bad Request
end
end
Service->>+Repo: update(id, dto)
Repo->>+DB: UPDATE projects SET ... WHERE id
DB-->>-Repo: updatedProject
Repo-->>-Service: updatedProject
Service-->>-Controller: AdminProjectResponseDto
Controller-->>-Client: 200 AdminProjectResponseDto

Soft-deletes a project by setting deletedAt to the current timestamp.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminProjectController
participant Service as AdminProjectService
participant Repo as AdminProjectRepository
participant DB as PostgreSQL
Client->>+Guard: DELETE /v1/admin/projects/:id (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: softDeleteOneById(id)
Service->>+Repo: findOne({ where: { id, deletedAt: null } })
Repo->>+DB: SELECT project
DB-->>-Repo: project | null
Repo-->>-Service: project | null
alt Project not found
Service-->>Controller: NotFoundException("Project not found")
Controller-->>Client: 404 Not Found
end
Service->>+Repo: update(id, { deletedAt: new Date() })
Repo->>+DB: UPDATE projects SET deletedAt = NOW() WHERE id
DB-->>-Repo: updatedProject
Repo-->>-Service: updatedProject
Service-->>-Controller: void
Controller-->>-Client: 204 No Content

Lists all users with optional filtering by role, blocked status, and email search. Returns a paginated response.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminUserController
participant Service as AdminUserService
participant Repo as AdminUserRepository
participant Mapper as MapperService
participant DB as PostgreSQL
Client->>+Guard: GET /v1/admin/users?role=X&isBlocked=true&email=Z&page=1&limit=10 (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: findAll(query)
Service->>Service: Build where clause
Note over Service: { ...(role: via userOnRole filter), ...(isBlocked), ...(email: contains/insensitive) }
Service->>+Repo: findManyAndPaginate({ where, include: { userOnRole: { include: { role: true } } }, paginate: getPaginate(query) })
Repo->>+DB: SELECT users with joins
DB-->>-Repo: rows + count
Repo-->>-Service: paginatedResult
Service->>+Mapper: toPaginateResponse(result, AdminUserResponseDto)
Mapper-->>-Service: paginatedResponse
Service-->>-Controller: paginatedResponse
Controller-->>-Client: 200 PaginatedResponse<AdminUserResponseDto>

Retrieves a single user by ID with full details including roles, profile, and associated projects.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminUserController
participant Service as AdminUserService
participant Repo as AdminUserRepository
participant DB as PostgreSQL
Client->>+Guard: GET /v1/admin/users/:id (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: findOneByIdOrFail(id)
Service->>+Repo: findOne({ where: { id }, include: { userOnRole: { include: { role: true } }, profile: true, customer: { include: { projects: true } } } })
Repo->>+DB: SELECT user with joins
DB-->>-Repo: user | null
Repo-->>-Service: user | null
alt User not found
Service-->>Controller: NotFoundException("User not found")
Controller-->>Client: 404 Not Found
else User found
Service-->>-Controller: AdminUserDetailResponseDto
Controller-->>-Client: 200 AdminUserDetailResponseDto
end

Blocks a non-admin user by setting isBlocked to true. Prevents blocking users with the ADMIN role.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminUserController
participant Service as AdminUserService
participant Repo as AdminUserRepository
participant DB as PostgreSQL
Client->>+Guard: PATCH /v1/admin/users/:id/block (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: blockUser(id)
Service->>+Repo: findOne({ where: { id }, include: { userOnRole: { include: { role: true } } } })
Repo->>+DB: SELECT user with roles
DB-->>-Repo: user | null
Repo-->>-Service: user | null
alt User not found
Service-->>Controller: NotFoundException("User not found")
Controller-->>Client: 404 Not Found
end
Service->>Service: Check if user has ADMIN role
alt User has ADMIN role
Service-->>Controller: ForbiddenException("Cannot block admin")
Controller-->>Client: 403 Forbidden
end
Service->>+Repo: update(id, { isBlocked: true })
Repo->>+DB: UPDATE users SET isBlocked = true WHERE id
DB-->>-Repo: updatedUser
Repo-->>-Service: updatedUser
Service-->>-Controller: { message: "User blocked" }
Controller-->>-Client: 200 { message: "User blocked" }

Unblocks a user by setting isBlocked to false.

sequenceDiagram
autonumber
actor Client
participant Guard as AdminAccessTokenGuard
participant Controller as AdminUserController
participant Service as AdminUserService
participant Repo as AdminUserRepository
participant DB as PostgreSQL
Client->>+Guard: PATCH /v1/admin/users/:id/unblock (Bearer accessToken)
Guard->>Guard: Validate access JWT with admin secret
alt Not authenticated
Guard-->>Client: 401 Unauthorized
else Authenticated
Guard->>+Controller: Request
end
Controller->>+Service: unblockUser(id)
Service->>+Repo: findOne({ where: { id } })
Repo->>+DB: SELECT user
DB-->>-Repo: user | null
Repo-->>-Service: user | null
alt User not found
Service-->>Controller: NotFoundException("User not found")
Controller-->>Client: 404 Not Found
end
Service->>+Repo: update(id, { isBlocked: false })
Repo->>+DB: UPDATE users SET isBlocked = false WHERE id
DB-->>-Repo: updatedUser
Repo-->>-Service: updatedUser
Service-->>-Controller: { message: "User unblocked" }
Controller-->>-Client: 200 { message: "User unblocked" }

Domain validation, user existence check, cooldown enforcement, code generation, Redis storage, and email dispatch.

flowchart TD
A["register(dto)"] --> B{"Is email domain\nin ADMIN_ALLOWED_DOMAINS?"}
B -- No --> C["Throw BadRequestException\n(Invalid email domain)"]
B -- Yes --> D["UserService.findOneByEmail(email)"]
D --> E{"User exists?"}
E -- Yes --> F["Throw ConflictException\n(User already exists)"]
E -- No --> G["Redis.get\nADMIN_VERIFICATION_RESEND_CODE:email"]
G --> H{"Cooldown active?"}
H -- Yes --> I["Throw TooManyRequestsException\n(Please wait before resending)"]
H -- No --> J["Generate code = getRandomCode(6)"]
J --> K["Redis.set\nADMIN_VERIFICATION_CODE:email\nTTL = 900s"]
K --> L["Redis.set\nADMIN_VERIFICATION_RESEND_CODE:email\nTTL = 120s"]
L --> M["EmailSenderService\n.sendAdminVerificationEmail(email, code)"]
M --> N["Return { message:\nVerification code sent }"]

Code validation, transactional user + role + session creation, and token pair generation.

flowchart TD
A["registerVerify(dto)"] --> B["Redis.get\nADMIN_VERIFICATION_CODE:email"]
B --> C{"Code found?"}
C -- No --> D["Throw NotFoundException\n(Verification code expired)"]
C -- Yes --> E{"Code matches\ndto.code?"}
E -- No --> F["Throw BadRequestException\n(Invalid verification code)"]
E -- Yes --> G["Redis.del\nADMIN_VERIFICATION_CODE:email"]
G --> H["BEGIN @Transactional()"]
H --> I["UserService.create\n({email}, no password)"]
I --> J["UserOnRole.create\n({userId, roleType: ADMIN})"]
J --> K["SessionService.create\n({userId, fingerprint})"]
K --> L["COMMIT @Transactional()"]
L --> M["TokenAdminService\n.generateAdminTokenPair(payload)"]
M --> N["TokenService.saveOne\n({sessionId, tokenId})"]
N --> O["Return {accessToken,\nrefreshToken}\n+ Set-Cookie"]

Project existence check followed by status transition validation against the allowed transitions map.

flowchart TD
A["updateOneById(id, dto)"] --> B["AdminProjectRepository.findOne\n({id, deletedAt: null})"]
B --> C{"Project found?"}
C -- No --> D["Throw NotFoundException\n(Project not found)"]
C -- Yes --> E{"dto.status\nprovided?"}
E -- No --> G["AdminProjectRepository\n.update(id, dto)"]
E -- Yes --> F{"Is dto.status in\nPROJECT_STATUS_TRANSITIONS\n[project.status]?"}
F -- No --> H["Throw BadRequestException\n(Invalid status transition\nfrom current to target)"]
F -- Yes --> G
G --> I["Return AdminProjectResponseDto"]

User existence check and admin role guard preventing admins from blocking other admins.

flowchart TD
A["blockUser(id)"] --> B["AdminUserRepository.findOne\n({id}, include: userOnRole.role)"]
B --> C{"User found?"}
C -- No --> D["Throw NotFoundException\n(User not found)"]
C -- Yes --> E{"User has\nADMIN role?"}
E -- Yes --> F["Throw ForbiddenException\n(Cannot block admin)"]
E -- No --> G["AdminUserRepository.update\n(id, {isBlocked: true})"]
G --> H["Return { message:\nUser blocked }"]

#EndpointData Stores TouchedSide Effects
1POST /admin/auth/registerRedis (read cooldown, write code + cooldown)Email sent
2POST /admin/auth/register/verifyRedis (read + delete code), PostgreSQL (insert user, user_on_role, session, token)Cookie set
3POST /admin/auth/loginPostgreSQL (read user + roles), Redis (read cooldown, write code + cooldown)Email sent
4POST /admin/auth/login/verifyRedis (read + delete code), PostgreSQL (read user, find/create session, upsert token)Cookie set
5POST /admin/auth/refreshPostgreSQL (read session, update token)Cookie set
6GET /admin/projectsPostgreSQL (read projects + customer + user)None
7GET /admin/projects/:idPostgreSQL (read project + customer + user)None
8PATCH /admin/projects/:idPostgreSQL (read + update project)None
9DELETE /admin/projects/:idPostgreSQL (read + soft-update project)None
10GET /admin/usersPostgreSQL (read users + roles)None
11GET /admin/users/:idPostgreSQL (read user + roles + profile + projects)None
12PATCH /admin/users/:id/blockPostgreSQL (read user + roles, update isBlocked)None
13PATCH /admin/users/:id/unblockPostgreSQL (read + update user)None