@flusys/nestjs-auth 1.0.0-beta → 1.0.0

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 (106) hide show
  1. package/README.md +590 -473
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +365 -24
  4. package/cjs/controllers/branch.controller.js +44 -1
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +44 -1
  7. package/cjs/controllers/user-permission.controller.js +82 -154
  8. package/cjs/controllers/user.controller.js +211 -7
  9. package/cjs/docs/auth-swagger.config.js +14 -7
  10. package/cjs/dtos/authentication.dto.js +114 -38
  11. package/cjs/dtos/index.js +0 -1
  12. package/cjs/dtos/user-permission.dto.js +28 -0
  13. package/cjs/dtos/user.dto.js +19 -4
  14. package/cjs/entities/index.js +5 -1
  15. package/cjs/entities/pending-registration.entity.js +125 -0
  16. package/cjs/entities/user-company-permission.entity.js +3 -9
  17. package/cjs/entities/user-token.entity.js +129 -0
  18. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  19. package/cjs/interceptors/set-token.interceptor.js +15 -55
  20. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  21. package/cjs/interfaces/index.js +2 -0
  22. package/cjs/interfaces/user-enricher.interface.js +4 -0
  23. package/cjs/modules/auth.module.js +65 -143
  24. package/cjs/services/auth-config.service.js +25 -27
  25. package/cjs/services/auth-datasource.provider.js +13 -23
  26. package/cjs/services/auth-email.service.js +456 -0
  27. package/cjs/services/authentication.service.js +297 -349
  28. package/cjs/services/branch.service.js +9 -4
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/services/user-permission.service.js +43 -73
  32. package/cjs/services/user.service.js +184 -34
  33. package/cjs/strategies/jwt.strategy.js +44 -44
  34. package/cjs/validators/password.validator.js +45 -0
  35. package/config/auth.constants.d.ts +3 -0
  36. package/controllers/authentication.controller.d.ts +16 -10
  37. package/controllers/company-selection.controller.d.ts +6 -2
  38. package/controllers/user-permission.controller.d.ts +5 -4
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +20 -6
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/index.d.ts +0 -1
  44. package/dtos/user-permission.dto.d.ts +4 -0
  45. package/dtos/user.dto.d.ts +1 -2
  46. package/entities/index.d.ts +7 -3
  47. package/entities/pending-registration.entity.d.ts +17 -0
  48. package/entities/user-company-permission.entity.d.ts +1 -3
  49. package/entities/user-token.entity.d.ts +15 -0
  50. package/fesm/config/auth.constants.js +6 -2
  51. package/fesm/controllers/authentication.controller.js +368 -27
  52. package/fesm/controllers/branch.controller.js +45 -2
  53. package/fesm/controllers/company-selection.controller.js +28 -21
  54. package/fesm/controllers/company.controller.js +45 -2
  55. package/fesm/controllers/user-permission.controller.js +84 -156
  56. package/fesm/controllers/user.controller.js +214 -10
  57. package/fesm/docs/auth-swagger.config.js +14 -7
  58. package/fesm/dtos/authentication.dto.js +101 -39
  59. package/fesm/dtos/index.js +0 -1
  60. package/fesm/dtos/user-permission.dto.js +28 -0
  61. package/fesm/dtos/user.dto.js +20 -5
  62. package/fesm/entities/index.js +7 -1
  63. package/fesm/entities/pending-registration.entity.js +115 -0
  64. package/fesm/entities/user-company-permission.entity.js +3 -9
  65. package/fesm/entities/user-token.entity.js +113 -0
  66. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  67. package/fesm/interceptors/set-token.interceptor.js +14 -54
  68. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  69. package/fesm/interfaces/authentication.interface.js +1 -1
  70. package/fesm/interfaces/index.js +2 -0
  71. package/fesm/interfaces/user-enricher.interface.js +26 -0
  72. package/fesm/modules/auth.module.js +67 -145
  73. package/fesm/services/auth-config.service.js +26 -28
  74. package/fesm/services/auth-datasource.provider.js +14 -24
  75. package/fesm/services/auth-email.service.js +405 -0
  76. package/fesm/services/authentication.service.js +297 -349
  77. package/fesm/services/branch.service.js +9 -4
  78. package/fesm/services/company-selection-session.service.js +1 -1
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/services/user-permission.service.js +44 -74
  81. package/fesm/services/user.service.js +186 -36
  82. package/fesm/strategies/jwt.strategy.js +44 -44
  83. package/fesm/validators/password.validator.js +48 -0
  84. package/interceptors/clear-token.interceptor.d.ts +0 -1
  85. package/interceptors/set-token.interceptor.d.ts +1 -1
  86. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  87. package/interfaces/auth-module-options.interface.d.ts +4 -1
  88. package/interfaces/authentication.interface.d.ts +13 -3
  89. package/interfaces/index.d.ts +2 -0
  90. package/interfaces/user-enricher.interface.d.ts +64 -0
  91. package/interfaces/user.interface.d.ts +1 -0
  92. package/modules/auth.module.d.ts +6 -3
  93. package/package.json +4 -4
  94. package/services/auth-config.service.d.ts +7 -8
  95. package/services/auth-datasource.provider.d.ts +2 -4
  96. package/services/auth-email.service.d.ts +54 -0
  97. package/services/authentication.service.d.ts +27 -16
  98. package/services/company-selection-session.service.d.ts +1 -1
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +6 -9
  101. package/services/user.service.d.ts +19 -3
  102. package/strategies/jwt.strategy.d.ts +4 -1
  103. package/validators/password.validator.d.ts +10 -0
  104. package/cjs/dtos/user-company-permission.dto.js +0 -110
  105. package/dtos/user-company-permission.dto.d.ts +0 -10
  106. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "BranchService", {
10
10
  });
11
11
  const _classes = require("@flusys/nestjs-shared/classes");
12
12
  const _modules = require("@flusys/nestjs-shared/modules");
13
+ const _utils = require("@flusys/nestjs-shared/utils");
13
14
  const _common = require("@nestjs/common");
14
15
  const _companybranchentity = require("../entities/company-branch.entity");
15
16
  const _authconfigservice = require("./auth-config.service");
@@ -76,10 +77,14 @@ let BranchService = class BranchService extends _classes.RequestScopedApiService
76
77
  }
77
78
  async getExtraManipulateQuery(query, filterDto, user) {
78
79
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
79
- if (this.authConfig.isCompanyFeatureEnabled() && user?.companyId) {
80
- query.andWhere('company_branch.company_id = :companyId', {
81
- companyId: user.companyId
82
- });
80
+ // Only apply company filter if companyId is NOT explicitly provided in filter
81
+ // This prevents dual filtering when user selects a specific company
82
+ if (!filterDto?.filter?.companyId) {
83
+ (0, _utils.applyCompanyFilter)(query, {
84
+ isCompanyFeatureEnabled: this.authConfig.isCompanyFeatureEnabled(),
85
+ entityAlias: 'company_branch',
86
+ columnName: 'company_id'
87
+ }, user);
83
88
  }
84
89
  return result;
85
90
  }
@@ -43,7 +43,7 @@ const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
43
43
  let CompanySelectionSessionService = class CompanySelectionSessionService {
44
44
  /**
45
45
  * Create a new session for company/branch selection
46
- */ async createSession(userId, companyId) {
46
+ */ async createSession(userId) {
47
47
  const sessionId = (0, _crypto.randomUUID)();
48
48
  const now = new Date();
49
49
  const expiresAt = new Date(now.getTime() + SESSION_TTL_MS);
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
5
  _export_star(require("./authentication.service"), exports);
6
+ _export_star(require("./auth-email.service"), exports);
6
7
  _export_star(require("./company-selection-session.service"), exports);
7
8
  _export_star(require("./user.service"), exports);
8
9
  _export_star(require("./company.service"), exports);
@@ -39,122 +39,92 @@ function _ts_param(paramIndex, decorator) {
39
39
  };
40
40
  }
41
41
  let UserPermissionService = class UserPermissionService {
42
- /** Get repository lazily - initialized on first access */ async getRepository() {
42
+ async getRepository() {
43
43
  if (!this._repository) {
44
44
  this._repository = await this.dataSourceProvider.getRepository(_usercompanypermissionentity.UserCompanyPermission);
45
45
  }
46
46
  return this._repository;
47
47
  }
48
- // Company Permissions
49
- /** Find all companies a user has access to */ async findUserCompanies(userId) {
48
+ // Generic Permission Operations
49
+ /** Find all permissions of a specific type for a user */ async findUserPermissions(userId, type) {
50
50
  const repository = await this.getRepository();
51
51
  return repository.find({
52
52
  where: {
53
53
  userId,
54
- permissionType: _usercompanypermissionentity.PermissionType.COMPANY
54
+ permissionType: type
55
55
  },
56
56
  relations: [
57
- 'company'
57
+ type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch'
58
58
  ],
59
59
  order: {
60
60
  createdAt: 'DESC'
61
61
  }
62
62
  });
63
63
  }
64
- /** Assign user to a company */ async assignUserToCompany(dto) {
64
+ /** Assign permission to user */ async assignPermission(userId, targetId, type, metadata) {
65
65
  const repository = await this.getRepository();
66
- // Check if already exists
66
+ const typeName = type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch';
67
67
  const existing = await repository.findOne({
68
68
  where: {
69
- userId: dto.userId,
70
- targetId: dto.companyId,
71
- permissionType: _usercompanypermissionentity.PermissionType.COMPANY
69
+ userId,
70
+ targetId,
71
+ permissionType: type
72
72
  }
73
73
  });
74
74
  if (existing) {
75
- throw new _common.BadRequestException('User already assigned to this company');
75
+ throw new _common.ConflictException(`User already assigned to this ${typeName}`);
76
76
  }
77
- const permission = repository.create({
78
- userId: dto.userId,
79
- permissionType: _usercompanypermissionentity.PermissionType.COMPANY,
80
- targetId: dto.companyId,
81
- metadata: dto.metadata
82
- });
83
- await repository.save(permission);
84
- }
85
- /** Revoke user access to a company */ async revokeCompanyAccess(userId, companyId) {
86
- const repository = await this.getRepository();
87
- const permission = await repository.findOne({
88
- where: {
77
+ try {
78
+ await repository.save(repository.create({
89
79
  userId,
90
- targetId: companyId,
91
- permissionType: _usercompanypermissionentity.PermissionType.COMPANY
80
+ permissionType: type,
81
+ targetId,
82
+ metadata
83
+ }));
84
+ } catch (error) {
85
+ if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
86
+ throw new _common.ConflictException(`User already assigned to this ${typeName}. Please refresh and try again.`);
92
87
  }
93
- });
94
- if (!permission) {
95
- throw new _common.NotFoundException('Company permission not found');
88
+ throw error;
96
89
  }
97
- await repository.remove(permission);
98
90
  }
99
- // Branch Permissions
100
- /** Find all branches a user has access to */ async findUserBranches(userId) {
101
- const repository = await this.getRepository();
102
- return repository.find({
103
- where: {
104
- userId,
105
- permissionType: _usercompanypermissionentity.PermissionType.BRANCH
106
- },
107
- relations: [
108
- 'branch'
109
- ],
110
- order: {
111
- createdAt: 'DESC'
112
- }
113
- });
114
- }
115
- /** Assign user to a branch */ async assignUserToBranch(dto) {
116
- const repository = await this.getRepository();
117
- // Check if already exists
118
- const existing = await repository.findOne({
119
- where: {
120
- userId: dto.userId,
121
- targetId: dto.branchId,
122
- permissionType: _usercompanypermissionentity.PermissionType.BRANCH
123
- }
124
- });
125
- if (existing) {
126
- throw new _common.BadRequestException('User already assigned to this branch');
127
- }
128
- const permission = repository.create({
129
- userId: dto.userId,
130
- permissionType: _usercompanypermissionentity.PermissionType.BRANCH,
131
- targetId: dto.branchId,
132
- metadata: dto.metadata
133
- });
134
- await repository.save(permission);
135
- }
136
- /** Revoke user access to a branch */ async revokeBranchAccess(userId, branchId) {
91
+ /** Revoke permission from user */ async revokePermission(userId, targetId, type) {
137
92
  const repository = await this.getRepository();
93
+ const typeName = type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch';
138
94
  const permission = await repository.findOne({
139
95
  where: {
140
96
  userId,
141
- targetId: branchId,
142
- permissionType: _usercompanypermissionentity.PermissionType.BRANCH
97
+ targetId,
98
+ permissionType: type
143
99
  }
144
100
  });
145
101
  if (!permission) {
146
- throw new _common.NotFoundException('Branch permission not found');
102
+ throw new _common.NotFoundException(`${typeName.charAt(0).toUpperCase() + typeName.slice(1)} permission not found`);
147
103
  }
148
104
  await repository.remove(permission);
149
105
  }
150
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
106
+ /** Find user branch permissions with optional company filter */ async findUserBranches(userId, companyId) {
107
+ const repository = await this.getRepository();
108
+ const queryBuilder = repository.createQueryBuilder('permission').leftJoinAndSelect('permission.branch', 'branch').where('permission.userId = :userId', {
109
+ userId
110
+ }).andWhere('permission.permissionType = :type', {
111
+ type: _usercompanypermissionentity.PermissionType.BRANCH
112
+ }).orderBy('permission.createdAt', 'DESC');
113
+ if (companyId) {
114
+ queryBuilder.andWhere('branch.companyId = :companyId', {
115
+ companyId
116
+ });
117
+ }
118
+ return queryBuilder.getMany();
119
+ }
151
120
  constructor(dataSourceProvider){
152
121
  _define_property(this, "dataSourceProvider", void 0);
153
- _define_property(this, "logger", void 0);
154
122
  _define_property(this, "_repository", void 0);
123
+ // Convenience Methods
124
+ _define_property(this, "findUserCompanies", void 0);
155
125
  this.dataSourceProvider = dataSourceProvider;
156
- this.logger = new _common.Logger(UserPermissionService.name);
157
126
  this._repository = null;
127
+ this.findUserCompanies = (userId)=>this.findUserPermissions(userId, _usercompanypermissionentity.PermissionType.COMPANY);
158
128
  }
159
129
  };
160
130
  UserPermissionService = _ts_decorate([
@@ -17,6 +17,7 @@ const _authconfigservice = require("./auth-config.service");
17
17
  const _authdatasourceprovider = require("./auth-datasource.provider");
18
18
  const _config = require("../config");
19
19
  const _enums = require("../enums");
20
+ const _userenricherinterface = require("../interfaces/user-enricher.interface");
20
21
  const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
21
22
  const _userentity = require("../entities/user.entity");
22
23
  function _define_property(obj, key, value) {
@@ -109,6 +110,24 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
109
110
  }
110
111
  }
111
112
  // Custom Methods
113
+ /**
114
+ * Validate profile ownership, ensure repository initialized, and fetch user.
115
+ * Throws ForbiddenException if not owner, NotFoundException if user not found.
116
+ */ async getUserForProfile(userId, user) {
117
+ if (userId !== user.id) {
118
+ throw new _common.ForbiddenException('Cannot access another user\'s profile');
119
+ }
120
+ await this.ensureRepositoryInitialized();
121
+ const userEntity = await this.repository.findOne({
122
+ where: {
123
+ id: userId
124
+ }
125
+ });
126
+ if (!userEntity) {
127
+ throw new _common.NotFoundException('User not found');
128
+ }
129
+ return userEntity;
130
+ }
112
131
  /** Verify email */ async verifyEmail(id) {
113
132
  // Ensure repository is initialized for custom method
114
133
  await this.ensureRepositoryInitialized();
@@ -170,33 +189,58 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
170
189
  /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
171
190
  // Repository initialization is handled automatically by RequestScopedApiService
172
191
  const entity = await super.convertSingleDtoToEntity(dto, user);
173
- // Hash password if provided
174
- if (dto.password) {
192
+ // Hash password if provided (CreateUserDto has password field)
193
+ if ('password' in dto && dto.password) {
175
194
  entity.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
176
195
  }
177
196
  return entity;
178
197
  }
179
198
  /** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
199
+ if (isRaw) {
200
+ return this.convertRawToResponseDto(entity);
201
+ }
202
+ return this.convertEntityToDto(entity);
203
+ }
204
+ /** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
180
205
  // When raw mode is used with company feature, isActive comes from computed field
181
- // Raw mode uses snake_case column names (due to SnakeNamingStrategy)
182
- const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : isRaw ? entity.user_is_active : entity.isActive;
206
+ const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
183
207
  return {
184
- id: isRaw ? entity.user_id : entity.id,
185
- name: isRaw ? entity.user_name : entity.name,
186
- phone: isRaw ? entity.user_phone : entity.phone,
187
- email: isRaw ? entity.user_email : entity.email,
188
- profilePictureId: isRaw ? entity.user_profile_picture_id : entity.profilePictureId,
208
+ id: raw.user_id,
209
+ name: raw.user_name,
210
+ phone: raw.user_phone,
211
+ email: raw.user_email,
212
+ profilePictureId: raw.user_profile_picture_id,
189
213
  isActive,
190
- emailVerified: isRaw ? entity.user_email_verified : entity.emailVerified,
191
- phoneVerified: isRaw ? entity.user_phone_verified : entity.phoneVerified,
192
- lastLoginAt: isRaw ? entity.user_last_login_at : entity.lastLoginAt,
193
- additionalFields: isRaw ? entity.user_additional_fields : entity.additionalFields,
194
- createdAt: isRaw ? entity.user_created_at : entity.createdAt,
195
- updatedAt: isRaw ? entity.user_updated_at : entity.updatedAt,
196
- deletedAt: isRaw ? entity.user_deleted_at : entity.deletedAt,
197
- createdById: isRaw ? entity.user_created_by_id : entity.createdById,
198
- updatedById: isRaw ? entity.user_updated_by_id : entity.updatedById,
199
- deletedById: isRaw ? entity.user_deleted_by_id : entity.deletedById
214
+ emailVerified: raw.user_email_verified,
215
+ phoneVerified: raw.user_phone_verified,
216
+ lastLoginAt: raw.user_last_login_at,
217
+ additionalFields: raw.user_additional_fields,
218
+ createdAt: raw.user_created_at,
219
+ updatedAt: raw.user_updated_at,
220
+ deletedAt: raw.user_deleted_at,
221
+ createdById: raw.user_created_by_id,
222
+ updatedById: raw.user_updated_by_id,
223
+ deletedById: raw.user_deleted_by_id
224
+ };
225
+ }
226
+ /** Convert entity to response DTO */ convertEntityToDto(entity) {
227
+ return {
228
+ id: entity.id,
229
+ name: entity.name,
230
+ phone: entity.phone,
231
+ email: entity.email,
232
+ profilePictureId: entity.profilePictureId,
233
+ isActive: entity.isActive,
234
+ emailVerified: entity.emailVerified,
235
+ phoneVerified: entity.phoneVerified,
236
+ lastLoginAt: entity.lastLoginAt,
237
+ additionalFields: entity.additionalFields,
238
+ createdAt: entity.createdAt,
239
+ updatedAt: entity.updatedAt,
240
+ deletedAt: entity.deletedAt,
241
+ createdById: entity.createdById,
242
+ updatedById: entity.updatedById,
243
+ deletedById: entity.deletedById
200
244
  };
201
245
  }
202
246
  /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
@@ -213,7 +257,11 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
213
257
  }
214
258
  /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
215
259
  // Repository initialization is handled automatically by RequestScopedApiService
216
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
260
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
261
+ // Apply user enricher query modifications if available
262
+ if (this.userEnricher?.enrichListQuery) {
263
+ await this.userEnricher.enrichListQuery(query, user);
264
+ }
217
265
  // Only apply company filtering if:
218
266
  // 1. Company feature is enabled at bootstrap
219
267
  // 2. User has company context
@@ -333,16 +381,105 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
333
381
  await queryRunner.release();
334
382
  }
335
383
  }
336
- /** Update user profile with password handling */ async profile(dto, user) {
337
- // Repository initialization is handled automatically by RequestScopedApiService
384
+ /**
385
+ * Get user profile with extras from enricher
386
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
387
+ */ async getProfileWithExtras(userId, user) {
388
+ const userEntity = await this.getUserForProfile(userId, user);
389
+ const userData = this.convertEntityToResponseDto(userEntity, false);
390
+ // Get extras from enricher if available
391
+ let extras;
392
+ if (this.userEnricher?.getProfileExtras) {
393
+ extras = await this.userEnricher.getProfileExtras(userId, user);
394
+ }
395
+ return {
396
+ user: userData,
397
+ extras
398
+ };
399
+ }
400
+ /**
401
+ * Get enriched user list
402
+ * Applies enricher transformations after query execution
403
+ */ async getEnrichedList(users, user) {
404
+ if (this.userEnricher?.enrichListItems) {
405
+ return this.userEnricher.enrichListItems(users, user);
406
+ }
407
+ return users;
408
+ }
409
+ // ============================================
410
+ // Multi-Step Profile Methods (via Enricher)
411
+ // ============================================
412
+ /**
413
+ * Get profile section definitions from enricher
414
+ */ async getProfileSections() {
415
+ if (this.userEnricher?.getProfileSections) {
416
+ return this.userEnricher.getProfileSections();
417
+ }
418
+ return [];
419
+ }
420
+ /**
421
+ * Get specific profile section data
422
+ */ async getProfileSectionData(userId, sectionId, user) {
423
+ await this.getUserForProfile(userId, user);
424
+ if (this.userEnricher?.getProfileSectionData) {
425
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
426
+ }
427
+ return null;
428
+ }
429
+ /**
430
+ * Update specific profile section
431
+ */ async updateProfileSection(userId, sectionId, data, user) {
432
+ const userEntity = await this.getUserForProfile(userId, user);
338
433
  if (!this.dataSource) {
339
434
  throw new _common.BadRequestException('DataSource not available');
340
435
  }
436
+ if (!this.userEnricher?.updateProfileSection) {
437
+ throw new _common.BadRequestException('Profile sections not supported');
438
+ }
439
+ const queryRunner = this.dataSource.createQueryRunner();
440
+ await queryRunner.connect();
441
+ await queryRunner.startTransaction();
442
+ try {
443
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
444
+ await queryRunner.commitTransaction();
445
+ await this.clearCacheForId([
446
+ userEntity
447
+ ]);
448
+ } catch (error) {
449
+ await queryRunner.rollbackTransaction();
450
+ _utils.ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
451
+ entity: this.entityName,
452
+ sectionId
453
+ });
454
+ _utils.ErrorHandler.rethrowError(error);
455
+ } finally{
456
+ await queryRunner.release();
457
+ }
458
+ }
459
+ /**
460
+ * Get profile completion percentage
461
+ */ async getProfileCompletion(userId, user) {
462
+ await this.getUserForProfile(userId, user);
463
+ if (this.userEnricher?.calculateProfileCompletion) {
464
+ return this.userEnricher.calculateProfileCompletion(userId, user);
465
+ }
466
+ return 100; // Default to 100% if no enricher
467
+ }
468
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
469
+ // Ensure repository and dataSource are initialized
470
+ await this.ensureRepositoryInitialized();
471
+ if (!this.dataSource) {
472
+ throw new _common.BadRequestException('DataSource not available');
473
+ }
474
+ const userId = dto.id ?? user.id;
475
+ // Validate extras if enricher provides validation
476
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
477
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
478
+ }
341
479
  const queryRunner = this.dataSource.createQueryRunner();
342
480
  await queryRunner.connect();
343
481
  await queryRunner.startTransaction();
344
482
  try {
345
- const userId = dto.id ?? user.id;
346
483
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
347
484
  where: {
348
485
  id: userId
@@ -353,18 +490,28 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
353
490
  }
354
491
  // Handle password update
355
492
  if (dto.password) {
356
- // If updating own profile, require old password
357
- if (!dto.id || dto.id === user.id) {
358
- // This would need oldPassword in DTO - for now, skip if not provided
359
- // In real implementation, add oldPassword to UpdateUserDto
493
+ // If updating own profile, require old password verification
494
+ const isOwnProfile = !dto.id || dto.id === user.id;
495
+ if (isOwnProfile) {
496
+ if (!dto.oldPassword) {
497
+ throw new _common.BadRequestException('Current password is required to change your password');
498
+ }
499
+ const isPasswordValid = await _bcrypt.compare(dto.oldPassword, findUser.password);
500
+ if (!isPasswordValid) {
501
+ throw new _common.BadRequestException('Current password is incorrect');
502
+ }
360
503
  }
361
- const salt = await _bcrypt.genSalt();
362
- findUser.password = await _bcrypt.hash(dto.password, salt);
504
+ findUser.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
363
505
  }
364
- // Merge other fields (excluding password)
365
- const { password, ...restDto } = dto;
506
+ // Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
507
+ // Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
508
+ const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
366
509
  Object.assign(findUser, restDto);
367
510
  await queryRunner.manager.save(this.repository.target, findUser);
511
+ // Handle extras update via enricher
512
+ if (extras && this.userEnricher?.updateProfileExtras) {
513
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
514
+ }
368
515
  await queryRunner.commitTransaction();
369
516
  await this.clearCacheForId([
370
517
  findUser
@@ -380,8 +527,8 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
380
527
  await queryRunner.release();
381
528
  }
382
529
  }
383
- constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
384
- 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, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig;
530
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
531
+ 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;
385
532
  }
386
533
  };
387
534
  UserService = _ts_decorate([
@@ -392,11 +539,14 @@ UserService = _ts_decorate([
392
539
  _ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
393
540
  _ts_param(2, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
394
541
  _ts_param(3, (0, _common.Inject)(_authconfigservice.AuthConfigService)),
542
+ _ts_param(4, (0, _common.Optional)()),
543
+ _ts_param(4, (0, _common.Inject)(_config.USER_ENRICHER)),
395
544
  _ts_metadata("design:type", Function),
396
545
  _ts_metadata("design:paramtypes", [
397
546
  typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
398
547
  typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
399
548
  typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider,
400
- typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService
549
+ typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService,
550
+ typeof _userenricherinterface.IUserEnricher === "undefined" ? Object : _userenricherinterface.IUserEnricher
401
551
  ])
402
552
  ], UserService);
@@ -12,10 +12,10 @@ const _common = require("@nestjs/common");
12
12
  const _core = require("@nestjs/core");
13
13
  const _passport = require("@nestjs/passport");
14
14
  const _passportjwt = require("passport-jwt");
15
+ const _userentity = require("../entities/user.entity");
15
16
  const _authconfigservice = require("../services/auth-config.service");
16
17
  const _authdatasourceprovider = require("../services/auth-datasource.provider");
17
18
  const _authenticationservice = require("../services/authentication.service");
18
- const _userentity = require("../entities/user.entity");
19
19
  function _define_property(obj, key, value) {
20
20
  if (key in obj) {
21
21
  Object.defineProperty(obj, key, {
@@ -38,71 +38,71 @@ function _ts_decorate(decorators, target, key, desc) {
38
38
  function _ts_metadata(k, v) {
39
39
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
40
40
  }
41
- /**
42
- * Type guard to check if payload includes company context
43
- */ function isTokenPayloadWithCompany(payload) {
41
+ function hasCompanyContext(payload) {
44
42
  return 'companyId' in payload && 'branchId' in payload;
45
43
  }
46
44
  let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_passportjwt.Strategy, 'jwt') {
47
45
  async validate(_req, payload) {
48
- // Resolve request-scoped services for this request
49
- let userRepository = null;
50
- let authService = null;
46
+ const userRepo = await this.resolveUserRepository();
47
+ await this.checkTokenRevocation(payload);
48
+ const user = await userRepo.findOne({
49
+ where: {
50
+ id: payload.id,
51
+ isActive: true
52
+ }
53
+ });
54
+ if (!user) {
55
+ throw new _common.UnauthorizedException('User not found or inactive');
56
+ }
57
+ return this.buildUserInfo(payload);
58
+ }
59
+ async resolveUserRepository() {
51
60
  try {
52
- const dataSourceProvider = await this.moduleRef.resolve(_authdatasourceprovider.AuthDataSourceProvider, undefined, {
61
+ const provider = await this.moduleRef.resolve(_authdatasourceprovider.AuthDataSourceProvider, undefined, {
53
62
  strict: false
54
63
  });
55
- if (dataSourceProvider) {
56
- userRepository = await dataSourceProvider.getRepository(_userentity.User);
64
+ if (provider) {
65
+ return provider.getRepository(_userentity.User);
57
66
  }
58
- // Resolve AuthenticationService for token revocation check
59
- authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
60
- strict: false
61
- });
62
- } catch (error) {
67
+ } catch {
63
68
  throw new _common.UnauthorizedException('Unable to access user data');
64
69
  }
65
- if (!userRepository) {
66
- throw new _common.UnauthorizedException('User repository not available');
67
- }
68
- // Check if token is revoked (logged out)
69
- if (authService && payload.iat) {
70
- const isRevoked = await authService.isTokenRevoked(payload.id, payload.iat);
71
- if (isRevoked) {
70
+ throw new _common.UnauthorizedException('User repository not available');
71
+ }
72
+ async checkTokenRevocation(payload) {
73
+ if (!payload.iat) return;
74
+ try {
75
+ const authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
76
+ strict: false
77
+ });
78
+ if (authService && await authService.isTokenRevoked(payload.id, payload.iat)) {
72
79
  throw new _common.UnauthorizedException('Token has been revoked');
73
80
  }
81
+ } catch (err) {
82
+ if (err instanceof _common.UnauthorizedException) throw err;
74
83
  }
75
- // Find user and check if active
76
- const user = await userRepository.findOne({
77
- where: {
78
- id: payload.id,
79
- isActive: true
80
- }
81
- });
82
- if (!user) {
83
- throw new _common.UnauthorizedException('User not found or inactive');
84
- }
85
- const result = {
84
+ }
85
+ buildUserInfo(payload) {
86
+ const base = {
86
87
  id: payload.id,
87
88
  email: payload.email
88
89
  };
89
- // Company context - use type guard for type safety
90
- if (isTokenPayloadWithCompany(payload)) {
91
- result.companyId = payload.companyId;
92
- // Only set branchId if it's not empty string
93
- if (payload.branchId) {
94
- result.branchId = payload.branchId;
95
- }
90
+ if (!hasCompanyContext(payload)) {
91
+ return base;
96
92
  }
97
- return result;
93
+ return {
94
+ ...base,
95
+ companyId: payload.companyId,
96
+ ...payload.branchId && {
97
+ branchId: payload.branchId
98
+ }
99
+ };
98
100
  }
99
101
  constructor(authConfig, moduleRef){
100
102
  super({
101
103
  jwtFromRequest: _passportjwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
102
104
  ignoreExpiration: false,
103
- // Use secretOrKeyProvider instead of secretOrKey to defer secret lookup
104
- // This ensures AuthConfigService is fully initialized when using forRootAsync
105
- secretOrKeyProvider: (_request, _rawJwtToken, done)=>{
105
+ secretOrKeyProvider: (_req, _token, done)=>{
106
106
  done(null, this.authConfig.getJwtSecret());
107
107
  },
108
108
  passReqToCallback: true