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

6. Domain Model

This document describes the domain model for the CrewsForge platform, with emphasis on the changes introduced by the Admin Service feature. All diagrams and definitions are framework-agnostic and reflect domain-level concepts rather than persistence details.


classDiagram
class User {
+UUID id
+String email
+String? phone
+String? passwordHash
+String? passwordSalt
+Boolean isEmailVerified
+Boolean isBlocked
+DateTime createdAt
+DateTime updatedAt
+block() void
+unblock() void
+isAdmin() Boolean
+hasPassword() Boolean
+canAuthenticate() Boolean
}
class Role {
+UUID id
+RoleType type
+DateTime createdAt
+DateTime updatedAt
}
class UserOnRole {
+UUID id
+UUID userId
+UUID roleId
+DateTime createdAt
+DateTime updatedAt
}
class Customer {
+UUID id
+UUID userId
+DateTime createdAt
+DateTime updatedAt
}
class Employee {
+UUID id
+UUID userId
+String? bio
+DateTime createdAt
+DateTime updatedAt
}
class Profile {
+UUID id
+UUID userId
+String? firstName
+String? lastName
+UUID? timezoneId
+UUID? locationId
+DateTime createdAt
+DateTime updatedAt
}
class Project {
+UUID id
+String title
+String description
+ProjectCategoryType? category
+ProjectStatus status
+UUID customerId
+DateTime? deletedAt
+DateTime createdAt
+DateTime updatedAt
+softDelete() void
+restore() void
+isDeleted() Boolean
+transitionStatus(ProjectStatus newStatus) void
+canTransitionTo(ProjectStatus newStatus) Boolean
}
class Session {
+UUID id
+UUID userId
+UUID tokenId
+String fingerprint
+String ip
+DateTime createdAt
+DateTime updatedAt
}
class Token {
+UUID id
+UUID userId
+UUID accessTokenId
+Int accessTokenExpiredAt
+String refreshTokenId
+Int refreshTokenExpiredAt
+DateTime createdAt
+DateTime updatedAt
}
class RoleType {
<<enumeration>>
CUSTOMER
EMPLOYEE
ADMIN
}
class ProjectStatus {
<<enumeration>>
DRAFT
PUBLISHED
HIRING
IN_PROGRESS
REVIEW
COMPLETED
CANCELLED
DISPUTED
}
class ProjectCategoryType {
<<enumeration>>
WEB_DEVELOPMENT
MOBILE_DEVELOPMENT
DESIGN
MARKETING
COPYWRITING
DATA_SCIENCE
DEV_OPS
QA_TESTING
OTHER
}
User "1" --> "0..1" Customer
User "1" --> "0..1" Employee
User "1" --> "0..1" Profile
User "1" --> "*" UserOnRole
User "1" --> "*" Token
User "1" --> "*" Session
UserOnRole "*" --> "1" Role
Role --> RoleType
Customer "1" --> "*" Project
Project --> ProjectStatus
Project --> ProjectCategoryType
Token "1" --> "0..1" Session

Value ObjectFieldsInvariants
TokenPayloaduserId: UUID, roles: RoleType[], customerId?: UUID, employeeId?: UUID, iat: number, exp: number, jti: UUIDMust contain at least one role. For ADMIN role, customerId and employeeId are always null. jti must be a valid UUID matching a Token record.
VerificationCodecode: String(6), email: String, ttl: 900s, cooldown: 120sExactly 6 alphanumeric characters. Stored in Redis with key pattern ADMIN_VERIFICATION_CODE:email={email}. One-time use — deleted from Redis immediately after successful verification. Cooldown key ADMIN_VERIFICATION_RESEND_CODE:email={email} prevents resending within 120 seconds.
AdminAllowedDomainsdomains: String[]Parsed from env variable ADMIN_ALLOWED_DOMAINS (comma-separated). Must contain at least one domain. Domain comparison is case-insensitive. Only the domain portion of the email (after @) is matched.
ProjectStatusTransitionfrom: ProjectStatus, to: ProjectStatus[]Transitions are strictly defined by the PROJECT_STATUS_TRANSITIONS map. COMPLETED and CANCELLED are terminal states with no outgoing transitions. Any transition not in the map is rejected.

graph TB
subgraph UserAggregate["User Aggregate (Root: User)"]
User["User"]
UserOnRole["UserOnRole"]
Session["Session"]
Token["Token"]
Profile["Profile"]
end
subgraph CustomerAggregate["Customer Aggregate (Root: Customer)"]
Customer["Customer"]
CustomerProfile["CustomerProfile"]
end
subgraph EmployeeAggregate["Employee Aggregate (Root: Employee)"]
Employee["Employee"]
EmployeeProfile["EmployeeProfile"]
end
subgraph ProjectAggregate["Project Aggregate (Root: Project)"]
Project["Project"]
WalriderThread["WalriderThread"]
WalriderMessage["WalriderMessage"]
WalriderThreadState["WalriderThreadState"]
end
subgraph RoleAggregate["Role Aggregate (Root: Role)"]
Role["Role"]
end
User -.->|"references"| Customer
User -.->|"references"| Employee
UserOnRole -.->|"references"| Role
Customer -.->|"owns"| Project

Aggregate rules:

  • User Aggregate — The User entity is the aggregate root. It owns UserOnRole (role assignments), Sessions, Tokens, and Profile. Admin is not a separate entity; it is a User with an ADMIN role assignment via UserOnRole. All blocking/unblocking operations go through the User root.
  • Customer Aggregate — Customer is the aggregate root. It is linked to User but managed independently. Owns CustomerProfile.
  • Employee Aggregate — Employee is the aggregate root. It is linked to User but managed independently. Owns EmployeeProfile and its nested structures (skills, specializations, portfolios).
  • Project Aggregate — Project is the aggregate root. Owns WalriderThread and its messages/states. Soft delete (deletedAt) and status transitions are enforced at the aggregate root level.
  • Role Aggregate — Role is a reference entity. Roles are system-defined (seeded) and immutable at runtime.

  1. Admin cannot block another admin. Before executing a block operation, the system must verify that the target user does not hold the ADMIN role. If the target has an ADMIN role via UserOnRole, the operation is rejected.

  2. Admin must register with a corporate email domain. The domain portion of the admin’s email must match one of the entries in ADMIN_ALLOWED_DOMAINS. Registration with a non-matching domain is rejected at the service layer before any user record is created.

  3. Verification code is one-time use. Upon successful verification, the code is immediately deleted from Redis. A second attempt to verify the same code will fail. This applies to both registration and login flows.

  4. Soft-deleted projects are excluded from all queries by default. Any query that retrieves projects must include a deletedAt IS NULL filter. This applies to both admin and non-admin endpoints. A project with a non-null deletedAt is considered logically deleted.

  5. Project status transitions must follow the defined transition map. A status change from state A to state B is permitted only if B is in the allowed targets for A as defined by PROJECT_STATUS_TRANSITIONS. Any transition not in the map is rejected with a validation error.

  6. COMPLETED and CANCELLED are terminal project states. Once a project reaches COMPLETED or CANCELLED status, no further status transitions are allowed. These states have empty transition target lists.

  7. Admin has no password. For users with only the ADMIN role, passwordHash and passwordSalt are null. Authentication is exclusively passwordless (email + verification code). The nullable password fields must not break existing login flows for Customer/Employee users.

  8. Blocked users cannot authenticate. When isBlocked is true, all authentication attempts (login, token refresh) must be rejected. Existing Passport strategies for Customer and Employee must check the isBlocked flag on every request.

  9. A user can hold multiple roles simultaneously. The UserOnRole junction table supports many-to-many relationships. A single user could theoretically hold CUSTOMER and EMPLOYEE roles (or any combination), but a user with ADMIN role should not also hold CUSTOMER or EMPLOYEE roles (admin is a distinct persona).

  10. Verification code cooldown prevents abuse. A new verification code cannot be sent to the same email within 120 seconds of the previous send. The cooldown is enforced via a separate Redis key with TTL of 120 seconds.

  11. All admins are equal — no hierarchy. There is no superadmin concept. Every user with the ADMIN role has identical permissions. Access control is binary: either a user has the ADMIN role or they do not.

  12. Session uniqueness is scoped to user and fingerprint. The composite unique constraint (userId, fingerprint) ensures one session per device per user. Creating a new session with the same fingerprint replaces the existing one.


EventTriggerPayloadConsumers
AdminRegisteredAdmin completes registration (email verified, user + role created){ userId, email, roles: ['ADMIN'], timestamp }Audit log (future), notification service
UserBlockedAdmin blocks a non-admin user{ userId, blockedByAdminId, email, timestamp }Auth service (invalidate active sessions), notification service
UserUnblockedAdmin unblocks a user{ userId, unblockedByAdminId, email, timestamp }Notification service
ProjectSoftDeletedAdmin soft-deletes a project{ projectId, deletedByAdminId, customerId, timestamp }Search index (remove from results), notification service
ProjectStatusChangedAdmin changes project status via transition{ projectId, previousStatus, newStatus, changedByAdminId, timestamp }Notification service, analytics
ProjectUpdatedByAdminAdmin edits project fields (title, description, category){ projectId, updatedFields: string[], changedByAdminId, timestamp }Audit log (future)
AdminLoginCompletedAdmin successfully logs in (code verified){ userId, email, ip, fingerprint, timestamp }Audit log (future), security monitoring

Note: Domain events are listed here for architectural completeness. The initial Admin Service MVP may not publish all events via a message broker. At minimum, service-layer method calls serve as synchronous event boundaries. Asynchronous event publishing via RabbitMQ can be introduced incrementally as consumer services are built.