User Management v1.0.0
Source: src/services/user.service.ts
Data Model
PolicyDefinition Usage
| Purpose | variant | subjectType | targetType |
|---|---|---|---|
| Assign role to user | group | User | Role |
| Map user to organizer | group | User | Organizer |
| Map user to merchant | group | User | Merchant |
| Grant permission to role | policy | Role | Permission |
| Grant permission to user | policy | User | Permission |
PolicyDefinition replaces the legacy
UserRoleandUserMappingtables with a single Casbin-style linking model.
UserService Dependencies
| Dependency | Purpose |
|---|---|
JWKSIssuerTokenService | JWT token operations |
UserConfigurationService | Post-create default configs |
UserRepository | User CRUD, transactions |
UserCredentialRepository | Password storage |
UserIdentifierRepository | Identifier uniqueness + CRUD |
UserProfileRepository | Profile CRUD |
PolicyDefinitionRepository | Role/org/merchant assignments |
User Creation Flow
Identifier Verification on Creation
| Scheme | verified | Can Sign-In Immediately |
|---|---|---|
USERNAME | true | Yes |
EMAIL | false | No — requires verification |
PHONE_NUMBER | false | No — requires verification |
Credential Handling
- If
credentialis provided → hashed withBun.password.hash()(Argon2id), stored asBASICscheme - If
credentialis omitted → step skipped entirely (e.g., customers without login)
User Update Flow
Updates are transactional with parallel sub-operations:
Identifier Update Logic
| Action | Detail |
|---|---|
| Add | New identifiers not in DB → create with verified: false |
| Remove | DB identifiers not in request → soft-delete |
| Username | Not updatable via this method |
Role Update Logic
Diff-based against PolicyDefinition records:
- Fetch current GROUP (USER→ROLE) policies
- Add: roleIds in request but not in DB → create new policies
- Remove: policies in DB but roleId not in request → soft-delete
Employee Management
Source: src/services/employee.service.ts
Create Employee
| Validation | Check | Error |
|---|---|---|
| Organizer access | CurrentUser has PolicyDef mapping to organizerId | 403 |
| Merchant ownership | All merchantIds belong to the organizer | 403 |
Update Employee
- Delegates user fields to
UserService.updateById() - Merchant mappings: delete-all-then-recreate strategy via PolicyDefinitionRepository
Request Schemas
CreateEmployeeRequest = CreateUserRequest + { organizerId, merchantIds[] }
UpdateByIdEmployeeRequest = UpdateByIdUserRequest + { merchantIds[]? } (all partial)
Employee Controller Authorization
All operations are scoped via UserRepository.findIdsByPoliciesForCurrentUser():
| Method | Scoping |
|---|---|
find / count / findOne | Filter by policy-scoped employee IDs + optional organizerId/merchantIds query params |
findById / deleteById | Verify ID is in policy-scoped set |
create | Validates organizer ownership in EmployeeService |
updateById | Validates organizer ownership in EmployeeService |
User Controller
| Route | Method | Description |
|---|---|---|
/users | GET | List users (paginated) |
/users/:id | GET | Get by ID |
/users | POST | Create → UserService.create() |
/users/:id | PATCH | Update → UserService.updateById() |
/users/:id | DELETE | Soft-delete |
/users/count | GET | Count |
/users/profile | GET | Current user's profile |
/users/profile | PATCH | Update current user's profile |
Request Schemas
CreateUserRequest
| Field | Type | Required | Validation |
|---|---|---|---|
username | string | No | 4–80 chars |
credential | string | No | 4–80 chars, PASSWORD_PATTERN |
emails | string[] | Yes | min 1, valid email |
phones | string[] | Yes | min 1, E.164 |
status | enum | Yes | ACTIVATED · DEACTIVATED · BLOCKED · UNKNOWN · ARCHIVED |
profile | object | Yes | firstName, lastName, birthday?, locale? |
roleIds | string[] | Yes | min 1 |
UpdateByIdUserRequest (all fields .partial())
| Field | Type | Validation |
|---|---|---|
emails | string[] | min 1 if provided |
phones | string[] | min 1 if provided |
status | enum | optional |
profile | object | partial update |
roleIds | string[] | min 1 if provided |
UpdateUserProfileRequest (for PATCH /profile)
| Field | Type |
|---|---|
emails | string[]? |
phones | string[]? |
status | enum? |
profile | { firstName?, lastName?, birthday?, locale?, metadata? } |
UserConfiguration Service
Source: src/services/user-configuration.service.ts
Called during _postCreate() after user creation:
UserConfiguration Controller
| Method | Path | Description |
|---|---|---|
| CRUD | /user-configurations | Standard operations |
POST | /user-configurations/views | Create config from template (validates code/name uniqueness per user) |
Repositories
UserRepository
findIdsByPoliciesForCurrentUser(opts: {
currentUserId: string;
organizerId?: string;
merchantIds?: string[];
roleIdentifiers?: string[];
transaction?: ITransaction;
}): Promise<string[]>SQL logic: PolicyDefinition (currentUser → Organizer) → JOIN PolicyDefinition (other users → same Organizer), optional merchant/role filters → distinct User IDs.
UserIdentifierRepository
validateIdentifierUniqueness(opts: {
identifiers: string[];
scheme?: string;
userId?: string; // exclude own identifiers during update
transaction?: ITransaction;
}): Promise<void> // throws if duplicates foundRelated Pages
- Authentication — Sign-in/up flow
- Customer Management — Customer-specific CRUD
- RBAC — PolicyDefinition model, role hierarchy
- Identity Overview — Architecture, components