@flusys/nestjs-auth 0.1.0-beta.3 → 1.1.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +1 -11
  2. package/cjs/config/auth.constants.js +14 -4
  3. package/cjs/controllers/authentication.controller.js +173 -36
  4. package/cjs/controllers/branch.controller.js +3 -4
  5. package/cjs/controllers/company.controller.js +3 -4
  6. package/cjs/controllers/user-permission.controller.js +6 -18
  7. package/cjs/controllers/user.controller.js +168 -22
  8. package/cjs/dtos/authentication.dto.js +106 -3
  9. package/cjs/entities/index.js +5 -1
  10. package/cjs/entities/pending-registration.entity.js +125 -0
  11. package/cjs/entities/user-token.entity.js +129 -0
  12. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  13. package/cjs/interfaces/authentication.interface.js +0 -3
  14. package/cjs/interfaces/index.js +2 -0
  15. package/cjs/interfaces/user-enricher.interface.js +4 -0
  16. package/cjs/modules/auth.module.js +8 -2
  17. package/cjs/services/auth-config.service.js +22 -44
  18. package/cjs/services/auth-datasource.provider.js +17 -33
  19. package/cjs/services/auth-email.service.js +452 -0
  20. package/cjs/services/authentication.service.js +229 -115
  21. package/cjs/services/branch.service.js +10 -44
  22. package/cjs/services/company.service.js +7 -36
  23. package/cjs/services/index.js +1 -0
  24. package/cjs/services/user-permission.service.js +9 -27
  25. package/cjs/services/user.service.js +177 -99
  26. package/cjs/strategies/jwt.strategy.js +14 -1
  27. package/config/auth.constants.d.ts +3 -0
  28. package/controllers/authentication.controller.d.ts +13 -9
  29. package/controllers/user.controller.d.ts +27 -0
  30. package/dtos/authentication.dto.d.ts +21 -3
  31. package/dtos/user.dto.d.ts +1 -0
  32. package/entities/index.d.ts +7 -3
  33. package/entities/pending-registration.entity.d.ts +17 -0
  34. package/entities/user-token.entity.d.ts +15 -0
  35. package/fesm/config/auth.constants.js +18 -8
  36. package/fesm/controllers/authentication.controller.js +174 -37
  37. package/fesm/controllers/branch.controller.js +5 -6
  38. package/fesm/controllers/company.controller.js +5 -6
  39. package/fesm/controllers/user-permission.controller.js +6 -18
  40. package/fesm/controllers/user.controller.js +168 -22
  41. package/fesm/dtos/authentication.dto.js +109 -100
  42. package/fesm/entities/index.js +7 -1
  43. package/fesm/entities/pending-registration.entity.js +115 -0
  44. package/fesm/entities/user-token.entity.js +113 -0
  45. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  46. package/fesm/interfaces/authentication.interface.js +1 -7
  47. package/fesm/interfaces/index.js +2 -0
  48. package/fesm/interfaces/user-enricher.interface.js +26 -0
  49. package/fesm/modules/auth.module.js +9 -3
  50. package/fesm/services/auth-config.service.js +22 -44
  51. package/fesm/services/auth-datasource.provider.js +17 -33
  52. package/fesm/services/auth-email.service.js +401 -0
  53. package/fesm/services/authentication.service.js +227 -113
  54. package/fesm/services/branch.service.js +11 -45
  55. package/fesm/services/company.service.js +8 -37
  56. package/fesm/services/index.js +1 -0
  57. package/fesm/services/user-permission.service.js +9 -27
  58. package/fesm/services/user.service.js +178 -100
  59. package/fesm/strategies/jwt.strategy.js +14 -1
  60. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  61. package/interfaces/auth-module-options.interface.d.ts +4 -1
  62. package/interfaces/authentication.interface.d.ts +14 -0
  63. package/interfaces/index.d.ts +2 -0
  64. package/interfaces/user-enricher.interface.d.ts +67 -0
  65. package/interfaces/user.interface.d.ts +1 -0
  66. package/package.json +3 -3
  67. package/services/auth-config.service.d.ts +1 -0
  68. package/services/auth-email.service.d.ts +50 -0
  69. package/services/authentication.service.d.ts +21 -5
  70. package/services/branch.service.d.ts +2 -5
  71. package/services/company.service.d.ts +1 -4
  72. package/services/index.d.ts +1 -0
  73. package/services/user.service.d.ts +17 -6
@@ -27,46 +27,20 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
- import { Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
30
+ import { Inject, Injectable, Scope } from '@nestjs/common';
31
31
  import { CompanyBranch } from '../entities/company-branch.entity';
32
32
  import { AuthConfigService } from './auth-config.service';
33
33
  import { AuthDataSourceProvider } from './auth-datasource.provider';
34
34
  export class BranchService extends RequestScopedApiService {
35
- /**
36
- * Resolve entity class for this service
37
- * @returns CompanyBranch entity class
38
- */ resolveEntity() {
35
+ resolveEntity() {
39
36
  return CompanyBranch;
40
37
  }
41
- /**
42
- * Get DataSource provider for this service
43
- * @returns AuthDataSourceProvider instance
44
- */ getDataSourceProvider() {
38
+ getDataSourceProvider() {
45
39
  return this.dataSourceProvider;
46
40
  }
47
- // ==================== Entity Conversion ====================
48
- async convertSingleDtoToEntity(dto, _user) {
49
- // Repository initialization is handled automatically by RequestScopedApiService
50
- let branch = new CompanyBranch();
51
- if ('id' in dto && dto.id && typeof dto.id === 'string') {
52
- const dbData = await this.repository.findOne({
53
- where: {
54
- id: dto.id
55
- }
56
- });
57
- if (!dbData) {
58
- throw new NotFoundException('Branch not found');
59
- }
60
- branch = dbData;
61
- }
62
- return {
63
- ...branch,
64
- ...dto
65
- };
66
- }
41
+ // Query Customization
67
42
  async getSelectQuery(query, _user, select) {
68
- // Repository initialization is handled automatically by RequestScopedApiService
69
- if (!select || !select.length) {
43
+ if (!select?.length) {
70
44
  select = [
71
45
  'id',
72
46
  'name',
@@ -75,14 +49,13 @@ export class BranchService extends RequestScopedApiService {
75
49
  'createdAt'
76
50
  ];
77
51
  }
78
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
79
- query.select(selectFields);
52
+ query.select(select.map((f)=>`${this.entityName}.${f}`));
80
53
  return {
81
54
  query,
82
55
  isRaw: false
83
56
  };
84
57
  }
85
- async getGlobalSearchQuery(query, search, _user) {
58
+ async getGlobalSearchQuery(query, search) {
86
59
  query.andWhere('(company_branch.name LIKE :search OR company_branch.slug LIKE :search OR company_branch.email LIKE :search)', {
87
60
  search: `%${search}%`
88
61
  });
@@ -91,24 +64,17 @@ export class BranchService extends RequestScopedApiService {
91
64
  isRaw: false
92
65
  };
93
66
  }
94
- /**
95
- * Override: Filter branches by user's company
96
- * Ensures users can only see branches belonging to their company
97
- */ async getExtraManipulateQuery(query, _filterAndPaginationDto, user) {
98
- const result = await super.getExtraManipulateQuery(query, _filterAndPaginationDto, user);
99
- // Only apply company filtering if company feature is enabled and user has company context
100
- const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
101
- if (isCompanyFeatureEnabled && user?.companyId) {
67
+ async getExtraManipulateQuery(query, filterDto, user) {
68
+ const result = await super.getExtraManipulateQuery(query, filterDto, user);
69
+ if (this.authConfig.isCompanyFeatureEnabled() && user?.companyId) {
102
70
  query.andWhere('company_branch.company_id = :companyId', {
103
71
  companyId: user.companyId
104
72
  });
105
73
  }
106
74
  return result;
107
75
  }
108
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
109
76
  constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
110
- // Repository will be set asynchronously by RequestScopedApiService
111
- super('company_branch', null, cacheManager, utilsService, BranchService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "logger", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.logger = new Logger(BranchService.name);
77
+ super('company_branch', null, cacheManager, utilsService, BranchService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig;
112
78
  }
113
79
  }
114
80
  BranchService = _ts_decorate([
@@ -27,45 +27,19 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
- import { Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
30
+ import { Inject, Injectable, Scope } from '@nestjs/common';
31
31
  import { Company } from '../entities/company.entity';
32
32
  import { AuthDataSourceProvider } from './auth-datasource.provider';
33
33
  export class CompanyService extends RequestScopedApiService {
34
- /**
35
- * Resolve entity class for this service
36
- * @returns Company entity class
37
- */ resolveEntity() {
34
+ resolveEntity() {
38
35
  return Company;
39
36
  }
40
- /**
41
- * Get DataSource provider for this service
42
- * @returns AuthDataSourceProvider instance
43
- */ getDataSourceProvider() {
37
+ getDataSourceProvider() {
44
38
  return this.dataSourceProvider;
45
39
  }
46
- // ==================== Entity Conversion ====================
47
- async convertSingleDtoToEntity(dto, _user) {
48
- // Repository initialization is handled automatically by RequestScopedApiService
49
- let company = new Company();
50
- if ('id' in dto && dto.id && typeof dto.id === 'string') {
51
- const dbData = await this.repository.findOne({
52
- where: {
53
- id: dto.id
54
- }
55
- });
56
- if (!dbData) {
57
- throw new NotFoundException('Company not found');
58
- }
59
- company = dbData;
60
- }
61
- return {
62
- ...company,
63
- ...dto
64
- };
65
- }
40
+ // Query Customization
66
41
  async getSelectQuery(query, _user, select) {
67
- // Repository initialization is handled automatically by RequestScopedApiService
68
- if (!select || !select.length) {
42
+ if (!select?.length) {
69
43
  select = [
70
44
  'id',
71
45
  'name',
@@ -74,14 +48,13 @@ export class CompanyService extends RequestScopedApiService {
74
48
  'createdAt'
75
49
  ];
76
50
  }
77
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
78
- query.select(selectFields);
51
+ query.select(select.map((f)=>`${this.entityName}.${f}`));
79
52
  return {
80
53
  query,
81
54
  isRaw: false
82
55
  };
83
56
  }
84
- async getGlobalSearchQuery(query, search, _user) {
57
+ async getGlobalSearchQuery(query, search) {
85
58
  query.andWhere('(company.name LIKE :search OR company.slug LIKE :search OR company.email LIKE :search)', {
86
59
  search: `%${search}%`
87
60
  });
@@ -90,10 +63,8 @@ export class CompanyService extends RequestScopedApiService {
90
63
  isRaw: false
91
64
  };
92
65
  }
93
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
94
66
  constructor(cacheManager, utilsService, dataSourceProvider){
95
- // Repository will be set asynchronously by RequestScopedApiService
96
- super('company', null, cacheManager, utilsService, CompanyService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "logger", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.logger = new Logger(CompanyService.name);
67
+ super('company', null, cacheManager, utilsService, CompanyService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider;
97
68
  }
98
69
  }
99
70
  CompanyService = _ts_decorate([
@@ -1,4 +1,5 @@
1
1
  export * from './authentication.service';
2
+ export * from './auth-email.service';
2
3
  export * from './company-selection-session.service';
3
4
  export * from './user.service';
4
5
  export * from './company.service';
@@ -29,20 +29,14 @@ import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Sco
29
29
  import { PermissionType, UserCompanyPermission } from '../entities/user-company-permission.entity';
30
30
  import { AuthDataSourceProvider } from './auth-datasource.provider';
31
31
  export class UserPermissionService {
32
- /**
33
- * Get repository lazily - initialized on first access
34
- */ async getRepository() {
32
+ /** Get repository lazily - initialized on first access */ async getRepository() {
35
33
  if (!this._repository) {
36
34
  this._repository = await this.dataSourceProvider.getRepository(UserCompanyPermission);
37
35
  }
38
36
  return this._repository;
39
37
  }
40
- // ==========================================================================
41
- // COMPANY PERMISSIONS
42
- // ==========================================================================
43
- /**
44
- * Find all companies a user has access to
45
- */ async findUserCompanies(userId) {
38
+ // Company Permissions
39
+ /** Find all companies a user has access to */ async findUserCompanies(userId) {
46
40
  const repository = await this.getRepository();
47
41
  return repository.find({
48
42
  where: {
@@ -57,9 +51,7 @@ export class UserPermissionService {
57
51
  }
58
52
  });
59
53
  }
60
- /**
61
- * Assign user to a company
62
- */ async assignUserToCompany(dto) {
54
+ /** Assign user to a company */ async assignUserToCompany(dto) {
63
55
  const repository = await this.getRepository();
64
56
  // Check if already exists
65
57
  const existing = await repository.findOne({
@@ -80,9 +72,7 @@ export class UserPermissionService {
80
72
  });
81
73
  await repository.save(permission);
82
74
  }
83
- /**
84
- * Revoke user access to a company
85
- */ async revokeCompanyAccess(userId, companyId) {
75
+ /** Revoke user access to a company */ async revokeCompanyAccess(userId, companyId) {
86
76
  const repository = await this.getRepository();
87
77
  const permission = await repository.findOne({
88
78
  where: {
@@ -96,12 +86,8 @@ export class UserPermissionService {
96
86
  }
97
87
  await repository.remove(permission);
98
88
  }
99
- // ==========================================================================
100
- // BRANCH PERMISSIONS
101
- // ==========================================================================
102
- /**
103
- * Find all branches a user has access to
104
- */ async findUserBranches(userId) {
89
+ // Branch Permissions
90
+ /** Find all branches a user has access to */ async findUserBranches(userId) {
105
91
  const repository = await this.getRepository();
106
92
  return repository.find({
107
93
  where: {
@@ -116,9 +102,7 @@ export class UserPermissionService {
116
102
  }
117
103
  });
118
104
  }
119
- /**
120
- * Assign user to a branch
121
- */ async assignUserToBranch(dto) {
105
+ /** Assign user to a branch */ async assignUserToBranch(dto) {
122
106
  const repository = await this.getRepository();
123
107
  // Check if already exists
124
108
  const existing = await repository.findOne({
@@ -139,9 +123,7 @@ export class UserPermissionService {
139
123
  });
140
124
  await repository.save(permission);
141
125
  }
142
- /**
143
- * Revoke user access to a branch
144
- */ async revokeBranchAccess(userId, branchId) {
126
+ /** Revoke user access to a branch */ async revokeBranchAccess(userId, branchId) {
145
127
  const repository = await this.getRepository();
146
128
  const permission = await repository.findOne({
147
129
  where: {
@@ -28,30 +28,23 @@ function _ts_param(paramIndex, decorator) {
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
30
  import { ErrorHandler } from '@flusys/nestjs-shared/utils';
31
- import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
31
+ import { BadRequestException, Inject, Injectable, NotFoundException, Optional, Scope } from '@nestjs/common';
32
32
  import * as bcrypt from 'bcrypt';
33
33
  import { AuthConfigService } from './auth-config.service';
34
34
  import { AuthDataSourceProvider } from './auth-datasource.provider';
35
+ import { BCRYPT_SALT_ROUNDS, USER_ENRICHER } from '../config';
35
36
  import { PermissionType } from '../enums';
37
+ import { IUserEnricher } from '../interfaces/user-enricher.interface';
36
38
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
37
39
  import { User } from '../entities/user.entity';
38
40
  export class UserService extends RequestScopedApiService {
39
- /**
40
- * Resolve entity class for this service
41
- * @returns User entity class
42
- */ resolveEntity() {
41
+ resolveEntity() {
43
42
  return User;
44
43
  }
45
- /**
46
- * Get DataSource provider for this service
47
- * @returns AuthDataSourceProvider instance
48
- */ getDataSourceProvider() {
44
+ getDataSourceProvider() {
49
45
  return this.dataSourceProvider;
50
46
  }
51
- /**
52
- * Override ensureRepositoryInitialized to also initialize permission repository and dataSource
53
- * CRITICAL: Also initializes permissionRepository and dataSource for company feature support
54
- */ async ensureRepositoryInitialized() {
47
+ async ensureRepositoryInitialized() {
55
48
  await super.ensureRepositoryInitialized();
56
49
  // Initialize permission repository if company feature is enabled
57
50
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -65,26 +58,8 @@ export class UserService extends RequestScopedApiService {
65
58
  this.dataSource = await this.getDataSourceForService();
66
59
  }
67
60
  }
68
- // ==========================================================================
69
- // CUSTOM METHODS (Beyond RequestScopedApiService)
70
- // ==========================================================================
71
- /**
72
- * Update last login timestamp
73
- */ async updateLastLogin(id) {
74
- // Ensure repository is initialized for custom method
75
- await this.ensureRepositoryInitialized();
76
- await this.repository.update(id, {
77
- lastLoginAt: new Date()
78
- });
79
- await this.clearCacheForId([
80
- {
81
- id
82
- }
83
- ]);
84
- }
85
- /**
86
- * Verify email
87
- */ async verifyEmail(id) {
61
+ // Custom Methods
62
+ /** Verify email */ async verifyEmail(id) {
88
63
  // Ensure repository is initialized for custom method
89
64
  await this.ensureRepositoryInitialized();
90
65
  await this.repository.update(id, {
@@ -97,9 +72,7 @@ export class UserService extends RequestScopedApiService {
97
72
  }
98
73
  ]);
99
74
  }
100
- /**
101
- * Verify phone
102
- */ async verifyPhone(id) {
75
+ /** Verify phone */ async verifyPhone(id) {
103
76
  // Ensure repository is initialized for custom method
104
77
  await this.ensureRepositoryInitialized();
105
78
  await this.repository.update(id, {
@@ -112,12 +85,7 @@ export class UserService extends RequestScopedApiService {
112
85
  }
113
86
  ]);
114
87
  }
115
- /**
116
- * Update user status
117
- *
118
- * When company feature is disabled: Updates User.isActive directly
119
- * When company feature is enabled: Updates UserCompanyPermission.isActive for the logged user's company
120
- */ async updateStatus(id, isActive, user) {
88
+ /** Update user status. When company feature enabled, updates permission isActive for logged user's company. */ async updateStatus(id, isActive, user) {
121
89
  // Ensure repository is initialized for custom method
122
90
  await this.ensureRepositoryInitialized();
123
91
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -148,50 +116,43 @@ export class UserService extends RequestScopedApiService {
148
116
  }
149
117
  ]);
150
118
  }
151
- // ==========================================================================
152
- // APISERVICE OVERRIDES (Customize behavior)
153
- // ==========================================================================
154
- /**
155
- * Override: Convert DTO to entity (hash password before saving)
156
- */ async convertSingleDtoToEntity(dto, user) {
119
+ // ApiService Overrides
120
+ /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
157
121
  // Repository initialization is handled automatically by RequestScopedApiService
158
122
  const entity = await super.convertSingleDtoToEntity(dto, user);
159
- // Hash password if provided
160
- if (dto.password) {
161
- entity.password = await bcrypt.hash(dto.password, 10);
123
+ // Hash password if provided (CreateUserDto has password field)
124
+ if ('password' in dto && dto.password) {
125
+ entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
162
126
  }
163
127
  return entity;
164
128
  }
165
- /**
166
- * Override: Convert entity to response DTO (remove password!)
167
- *
168
- * When company feature is enabled and raw mode is used, isActive comes from
169
- * the permission table (user_companyIsActive). Otherwise, it comes from User entity.
170
- */ convertEntityToResponseDto(entity, isRaw) {
129
+ /** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
171
130
  // When raw mode is used with company feature, isActive comes from computed field
172
- const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : entity.isActive;
131
+ // Raw mode uses snake_case column names (due to SnakeNamingStrategy)
132
+ const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : isRaw ? entity.user_is_active : entity.isActive;
173
133
  return {
174
134
  id: isRaw ? entity.user_id : entity.id,
175
135
  name: isRaw ? entity.user_name : entity.name,
176
136
  phone: isRaw ? entity.user_phone : entity.phone,
177
137
  email: isRaw ? entity.user_email : entity.email,
178
- profilePictureId: isRaw ? entity.user_profilePictureId : entity.profilePictureId,
138
+ profilePictureId: isRaw ? entity.user_profile_picture_id : entity.profilePictureId,
179
139
  isActive,
180
- emailVerified: isRaw ? entity.user_emailVerified : entity.emailVerified,
181
- phoneVerified: isRaw ? entity.user_phoneVerified : entity.phoneVerified,
182
- lastLoginAt: isRaw ? entity.user_lastLoginAt : entity.lastLoginAt,
183
- additionalFields: isRaw ? entity.user_additionalFields : entity.additionalFields,
184
- createdAt: isRaw ? entity.user_createdAt : entity.createdAt,
185
- updatedAt: isRaw ? entity.user_updatedAt : entity.updatedAt,
186
- deletedAt: isRaw ? entity.user_deletedAt : entity.deletedAt,
187
- createdById: isRaw ? entity.user_createdById : entity.createdById,
188
- updatedById: isRaw ? entity.user_updatedById : entity.updatedById,
189
- deletedById: isRaw ? entity.user_deletedById : entity.deletedById
140
+ emailVerified: isRaw ? entity.user_email_verified : entity.emailVerified,
141
+ phoneVerified: isRaw ? entity.user_phone_verified : entity.phoneVerified,
142
+ lastLoginAt: isRaw ? entity.user_last_login_at : entity.lastLoginAt,
143
+ additionalFields: isRaw ? entity.user_additional_fields : entity.additionalFields,
144
+ createdAt: isRaw ? entity.user_created_at : entity.createdAt,
145
+ updatedAt: isRaw ? entity.user_updated_at : entity.updatedAt,
146
+ deletedAt: isRaw ? entity.user_deleted_at : entity.deletedAt,
147
+ createdById: isRaw ? entity.user_created_by_id : entity.createdById,
148
+ updatedById: isRaw ? entity.user_updated_by_id : entity.updatedById,
149
+ deletedById: isRaw ? entity.user_deleted_by_id : entity.deletedById
190
150
  };
191
151
  }
192
- /**
193
- * Override: Global search - search by name, email, or phone
194
- */ async getGlobalSearchQuery(query, search, _user) {
152
+ /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
153
+ return entities.map((entity)=>this.convertEntityToResponseDto(entity, isRaw));
154
+ }
155
+ /** Global search - search by name, email, or phone */ async getGlobalSearchQuery(query, search, _user) {
195
156
  query.andWhere('(user.name LIKE :search OR user.email LIKE :search OR user.phone LIKE :search)', {
196
157
  search: `%${search}%`
197
158
  });
@@ -200,18 +161,13 @@ export class UserService extends RequestScopedApiService {
200
161
  isRaw: false
201
162
  };
202
163
  }
203
- /**
204
- * Override: getExtraManipulateQuery - Add company permission-based isActive check when company feature enabled
205
- *
206
- * Note: When company feature is disabled at bootstrap, this method returns without modifications.
207
- * All company-related filtering is skipped, and users are returned normally with isActive from User entity.
208
- *
209
- * When company feature is enabled:
210
- * - isActive is computed from UserCompanyPermission table
211
- * - Returns isRaw: true to use raw query results
212
- */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
164
+ /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
213
165
  // Repository initialization is handled automatically by RequestScopedApiService
214
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
166
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
167
+ // Apply user enricher query modifications if available
168
+ if (this.userEnricher?.enrichListQuery) {
169
+ await this.userEnricher.enrichListQuery(query, user);
170
+ }
215
171
  // Only apply company filtering if:
216
172
  // 1. Company feature is enabled at bootstrap
217
173
  // 2. User has company context
@@ -264,12 +220,7 @@ export class UserService extends RequestScopedApiService {
264
220
  }
265
221
  return result;
266
222
  }
267
- /**
268
- * Override: insert - Create company permission when company feature is enabled
269
- *
270
- * Note: When company feature is disabled at bootstrap, this method falls back to parent implementation.
271
- * No company permissions are created, and user is inserted normally.
272
- */ async insert(dto, user) {
223
+ /** Insert user. Creates company permission when company feature is enabled. */ async insert(dto, user) {
273
224
  // Repository initialization is handled automatically by RequestScopedApiService
274
225
  // Check if company feature is enabled
275
226
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -337,17 +288,139 @@ export class UserService extends RequestScopedApiService {
337
288
  }
338
289
  }
339
290
  /**
340
- * Update user profile with password handling
341
- */ async profile(dto, user) {
342
- // Repository initialization is handled automatically by RequestScopedApiService
291
+ * Get user profile with extras from enricher
292
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
293
+ */ async getProfileWithExtras(userId, user) {
294
+ await this.ensureRepositoryInitialized();
295
+ const userEntity = await this.repository.findOne({
296
+ where: {
297
+ id: userId
298
+ }
299
+ });
300
+ if (!userEntity) {
301
+ throw new NotFoundException('User not found');
302
+ }
303
+ const userData = this.convertEntityToResponseDto(userEntity, false);
304
+ // Get extras from enricher if available
305
+ let extras;
306
+ if (this.userEnricher?.getProfileExtras) {
307
+ extras = await this.userEnricher.getProfileExtras(userId, user);
308
+ }
309
+ return {
310
+ user: userData,
311
+ extras
312
+ };
313
+ }
314
+ /**
315
+ * Get enriched user list
316
+ * Applies enricher transformations after query execution
317
+ */ async getEnrichedList(users, user) {
318
+ if (this.userEnricher?.enrichListItems) {
319
+ return this.userEnricher.enrichListItems(users, user);
320
+ }
321
+ return users;
322
+ }
323
+ // ============================================
324
+ // Multi-Step Profile Methods (via Enricher)
325
+ // ============================================
326
+ /**
327
+ * Get profile section definitions from enricher
328
+ */ async getProfileSections() {
329
+ if (this.userEnricher?.getProfileSections) {
330
+ return this.userEnricher.getProfileSections();
331
+ }
332
+ return [];
333
+ }
334
+ /**
335
+ * Get specific profile section data
336
+ */ async getProfileSectionData(userId, sectionId, user) {
337
+ await this.ensureRepositoryInitialized();
338
+ // Verify user exists
339
+ const userEntity = await this.repository.findOne({
340
+ where: {
341
+ id: userId
342
+ }
343
+ });
344
+ if (!userEntity) {
345
+ throw new NotFoundException('User not found');
346
+ }
347
+ if (this.userEnricher?.getProfileSectionData) {
348
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
349
+ }
350
+ return null;
351
+ }
352
+ /**
353
+ * Update specific profile section
354
+ */ async updateProfileSection(userId, sectionId, data, user) {
355
+ await this.ensureRepositoryInitialized();
356
+ if (!this.dataSource) {
357
+ throw new BadRequestException('DataSource not available');
358
+ }
359
+ // Verify user exists
360
+ const userEntity = await this.repository.findOne({
361
+ where: {
362
+ id: userId
363
+ }
364
+ });
365
+ if (!userEntity) {
366
+ throw new NotFoundException('User not found');
367
+ }
368
+ if (!this.userEnricher?.updateProfileSection) {
369
+ throw new BadRequestException('Profile sections not supported');
370
+ }
371
+ const queryRunner = this.dataSource.createQueryRunner();
372
+ await queryRunner.connect();
373
+ await queryRunner.startTransaction();
374
+ try {
375
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
376
+ await queryRunner.commitTransaction();
377
+ await this.clearCacheForId([
378
+ userEntity
379
+ ]);
380
+ } catch (error) {
381
+ await queryRunner.rollbackTransaction();
382
+ ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
383
+ entity: this.entityName,
384
+ sectionId
385
+ });
386
+ ErrorHandler.rethrowError(error);
387
+ } finally{
388
+ await queryRunner.release();
389
+ }
390
+ }
391
+ /**
392
+ * Get profile completion percentage
393
+ */ async getProfileCompletion(userId, user) {
394
+ await this.ensureRepositoryInitialized();
395
+ // Verify user exists
396
+ const userEntity = await this.repository.findOne({
397
+ where: {
398
+ id: userId
399
+ }
400
+ });
401
+ if (!userEntity) {
402
+ throw new NotFoundException('User not found');
403
+ }
404
+ if (this.userEnricher?.calculateProfileCompletion) {
405
+ return this.userEnricher.calculateProfileCompletion(userId, user);
406
+ }
407
+ return 100; // Default to 100% if no enricher
408
+ }
409
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
410
+ // Ensure repository and dataSource are initialized
411
+ await this.ensureRepositoryInitialized();
343
412
  if (!this.dataSource) {
344
413
  throw new BadRequestException('DataSource not available');
345
414
  }
415
+ const userId = dto.id ?? user.id;
416
+ // Validate extras if enricher provides validation
417
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
418
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
419
+ }
346
420
  const queryRunner = this.dataSource.createQueryRunner();
347
421
  await queryRunner.connect();
348
422
  await queryRunner.startTransaction();
349
423
  try {
350
- const userId = dto.id ?? user.id;
351
424
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
352
425
  where: {
353
426
  id: userId
@@ -366,10 +439,14 @@ export class UserService extends RequestScopedApiService {
366
439
  const salt = await bcrypt.genSalt();
367
440
  findUser.password = await bcrypt.hash(dto.password, salt);
368
441
  }
369
- // Merge other fields (excluding password)
370
- const { password, ...restDto } = dto;
442
+ // Merge other fields (excluding password and extras)
443
+ const { password, extras, ...restDto } = dto;
371
444
  Object.assign(findUser, restDto);
372
445
  await queryRunner.manager.save(this.repository.target, findUser);
446
+ // Handle extras update via enricher
447
+ if (extras && this.userEnricher?.updateProfileExtras) {
448
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
449
+ }
373
450
  await queryRunner.commitTransaction();
374
451
  await this.clearCacheForId([
375
452
  findUser
@@ -385,10 +462,8 @@ export class UserService extends RequestScopedApiService {
385
462
  await queryRunner.release();
386
463
  }
387
464
  }
388
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
389
- constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
390
- // Repository will be set asynchronously by RequestScopedApiService
391
- super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "logger", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.logger = new Logger(UserService.name);
465
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
466
+ super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "userEnricher", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.userEnricher = userEnricher;
392
467
  }
393
468
  }
394
469
  UserService = _ts_decorate([
@@ -399,11 +474,14 @@ UserService = _ts_decorate([
399
474
  _ts_param(1, Inject(UtilsService)),
400
475
  _ts_param(2, Inject(AuthDataSourceProvider)),
401
476
  _ts_param(3, Inject(AuthConfigService)),
477
+ _ts_param(4, Optional()),
478
+ _ts_param(4, Inject(USER_ENRICHER)),
402
479
  _ts_metadata("design:type", Function),
403
480
  _ts_metadata("design:paramtypes", [
404
481
  typeof HybridCache === "undefined" ? Object : HybridCache,
405
482
  typeof UtilsService === "undefined" ? Object : UtilsService,
406
483
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
407
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
484
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
485
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher
408
486
  ])
409
487
  ], UserService);