@flusys/nestjs-auth 0.1.0-beta.3 → 1.0.0-rc

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 (102) hide show
  1. package/README.md +274 -301
  2. package/cjs/config/auth.constants.js +22 -4
  3. package/cjs/controllers/authentication.controller.js +222 -38
  4. package/cjs/controllers/branch.controller.js +46 -4
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +46 -4
  7. package/cjs/controllers/user-permission.controller.js +40 -37
  8. package/cjs/controllers/user.controller.js +222 -23
  9. package/cjs/dtos/authentication.dto.js +119 -13
  10. package/cjs/dtos/user-permission.dto.js +28 -0
  11. package/cjs/dtos/user.dto.js +19 -4
  12. package/cjs/entities/index.js +5 -1
  13. package/cjs/entities/pending-registration.entity.js +125 -0
  14. package/cjs/entities/user-company-permission.entity.js +0 -6
  15. package/cjs/entities/user-token.entity.js +129 -0
  16. package/cjs/guards/auth-throttler.guard.js +53 -0
  17. package/cjs/interceptors/clear-token.interceptor.js +3 -6
  18. package/cjs/interceptors/set-token.interceptor.js +38 -31
  19. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  20. package/cjs/interfaces/authentication.interface.js +0 -3
  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 +8 -20
  24. package/cjs/services/auth-config.service.js +42 -47
  25. package/cjs/services/auth-datasource.provider.js +18 -34
  26. package/cjs/services/auth-email.service.js +471 -0
  27. package/cjs/services/authentication.service.js +240 -153
  28. package/cjs/services/branch.service.js +15 -48
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/company.service.js +7 -36
  31. package/cjs/services/index.js +1 -0
  32. package/cjs/services/user-permission.service.js +9 -29
  33. package/cjs/services/user.service.js +228 -107
  34. package/cjs/strategies/jwt.strategy.js +14 -1
  35. package/cjs/validators/password.validator.js +45 -0
  36. package/config/auth.constants.d.ts +5 -0
  37. package/controllers/authentication.controller.d.ts +13 -9
  38. package/controllers/company-selection.controller.d.ts +6 -2
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +21 -3
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/user-permission.dto.d.ts +4 -0
  44. package/dtos/user.dto.d.ts +1 -2
  45. package/entities/index.d.ts +7 -3
  46. package/entities/pending-registration.entity.d.ts +17 -0
  47. package/entities/user-company-permission.entity.d.ts +0 -2
  48. package/entities/user-token.entity.d.ts +15 -0
  49. package/fesm/config/auth.constants.js +11 -8
  50. package/fesm/controllers/authentication.controller.js +224 -40
  51. package/fesm/controllers/branch.controller.js +49 -7
  52. package/fesm/controllers/company-selection.controller.js +28 -21
  53. package/fesm/controllers/company.controller.js +49 -7
  54. package/fesm/controllers/user-permission.controller.js +41 -38
  55. package/fesm/controllers/user.controller.js +225 -26
  56. package/fesm/dtos/authentication.dto.js +123 -111
  57. package/fesm/dtos/user-permission.dto.js +28 -0
  58. package/fesm/dtos/user.dto.js +20 -5
  59. package/fesm/entities/index.js +7 -1
  60. package/fesm/entities/pending-registration.entity.js +115 -0
  61. package/fesm/entities/user-company-permission.entity.js +0 -6
  62. package/fesm/entities/user-token.entity.js +113 -0
  63. package/fesm/guards/auth-throttler.guard.js +43 -0
  64. package/fesm/interceptors/clear-token.interceptor.js +2 -5
  65. package/fesm/interceptors/set-token.interceptor.js +37 -30
  66. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  67. package/fesm/interfaces/authentication.interface.js +1 -7
  68. package/fesm/interfaces/index.js +2 -0
  69. package/fesm/interfaces/user-enricher.interface.js +26 -0
  70. package/fesm/modules/auth.module.js +10 -22
  71. package/fesm/services/auth-config.service.js +42 -47
  72. package/fesm/services/auth-datasource.provider.js +19 -35
  73. package/fesm/services/auth-email.service.js +420 -0
  74. package/fesm/services/authentication.service.js +238 -151
  75. package/fesm/services/branch.service.js +16 -49
  76. package/fesm/services/company-selection-session.service.js +1 -1
  77. package/fesm/services/company.service.js +8 -37
  78. package/fesm/services/index.js +1 -0
  79. package/fesm/services/user-permission.service.js +10 -30
  80. package/fesm/services/user.service.js +229 -108
  81. package/fesm/strategies/jwt.strategy.js +14 -1
  82. package/fesm/validators/password.validator.js +48 -0
  83. package/guards/auth-throttler.guard.d.ts +7 -0
  84. package/interceptors/set-token.interceptor.d.ts +1 -0
  85. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  86. package/interfaces/auth-module-options.interface.d.ts +4 -1
  87. package/interfaces/authentication.interface.d.ts +14 -0
  88. package/interfaces/index.d.ts +2 -0
  89. package/interfaces/user-enricher.interface.d.ts +64 -0
  90. package/interfaces/user.interface.d.ts +1 -0
  91. package/modules/auth.module.d.ts +0 -1
  92. package/package.json +4 -4
  93. package/services/auth-config.service.d.ts +5 -1
  94. package/services/auth-email.service.d.ts +51 -0
  95. package/services/authentication.service.d.ts +21 -12
  96. package/services/branch.service.d.ts +2 -5
  97. package/services/company-selection-session.service.d.ts +1 -1
  98. package/services/company.service.d.ts +1 -4
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +0 -1
  101. package/services/user.service.d.ts +20 -6
  102. package/validators/password.validator.d.ts +10 -0
@@ -15,7 +15,9 @@ const _common = require("@nestjs/common");
15
15
  const _bcrypt = /*#__PURE__*/ _interop_require_wildcard(require("bcrypt"));
16
16
  const _authconfigservice = require("./auth-config.service");
17
17
  const _authdatasourceprovider = require("./auth-datasource.provider");
18
+ const _config = require("../config");
18
19
  const _enums = require("../enums");
20
+ const _userenricherinterface = require("../interfaces/user-enricher.interface");
19
21
  const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
20
22
  const _userentity = require("../entities/user.entity");
21
23
  function _define_property(obj, key, value) {
@@ -87,22 +89,13 @@ function _ts_param(paramIndex, decorator) {
87
89
  };
88
90
  }
89
91
  let UserService = class UserService extends _classes.RequestScopedApiService {
90
- /**
91
- * Resolve entity class for this service
92
- * @returns User entity class
93
- */ resolveEntity() {
92
+ resolveEntity() {
94
93
  return _userentity.User;
95
94
  }
96
- /**
97
- * Get DataSource provider for this service
98
- * @returns AuthDataSourceProvider instance
99
- */ getDataSourceProvider() {
95
+ getDataSourceProvider() {
100
96
  return this.dataSourceProvider;
101
97
  }
102
- /**
103
- * Override ensureRepositoryInitialized to also initialize permission repository and dataSource
104
- * CRITICAL: Also initializes permissionRepository and dataSource for company feature support
105
- */ async ensureRepositoryInitialized() {
98
+ async ensureRepositoryInitialized() {
106
99
  await super.ensureRepositoryInitialized();
107
100
  // Initialize permission repository if company feature is enabled
108
101
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -116,26 +109,16 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
116
109
  this.dataSource = await this.getDataSourceForService();
117
110
  }
118
111
  }
119
- // ==========================================================================
120
- // CUSTOM METHODS (Beyond RequestScopedApiService)
121
- // ==========================================================================
112
+ // Custom Methods
122
113
  /**
123
- * Update last login timestamp
124
- */ async updateLastLogin(id) {
125
- // Ensure repository is initialized for custom method
126
- await this.ensureRepositoryInitialized();
127
- await this.repository.update(id, {
128
- lastLoginAt: new Date()
129
- });
130
- await this.clearCacheForId([
131
- {
132
- id
133
- }
134
- ]);
114
+ * Validate that the requesting user owns the target profile.
115
+ * Users can only access/modify their own profile data.
116
+ */ validateProfileOwnership(targetUserId, requestingUser) {
117
+ if (targetUserId !== requestingUser.id) {
118
+ throw new _common.ForbiddenException('Cannot access another user\'s profile');
119
+ }
135
120
  }
136
- /**
137
- * Verify email
138
- */ async verifyEmail(id) {
121
+ /** Verify email */ async verifyEmail(id) {
139
122
  // Ensure repository is initialized for custom method
140
123
  await this.ensureRepositoryInitialized();
141
124
  await this.repository.update(id, {
@@ -148,9 +131,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
148
131
  }
149
132
  ]);
150
133
  }
151
- /**
152
- * Verify phone
153
- */ async verifyPhone(id) {
134
+ /** Verify phone */ async verifyPhone(id) {
154
135
  // Ensure repository is initialized for custom method
155
136
  await this.ensureRepositoryInitialized();
156
137
  await this.repository.update(id, {
@@ -163,12 +144,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
163
144
  }
164
145
  ]);
165
146
  }
166
- /**
167
- * Update user status
168
- *
169
- * When company feature is disabled: Updates User.isActive directly
170
- * When company feature is enabled: Updates UserCompanyPermission.isActive for the logged user's company
171
- */ async updateStatus(id, isActive, user) {
147
+ /** Update user status. When company feature enabled, updates permission isActive for logged user's company. */ async updateStatus(id, isActive, user) {
172
148
  // Ensure repository is initialized for custom method
173
149
  await this.ensureRepositoryInitialized();
174
150
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -199,50 +175,68 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
199
175
  }
200
176
  ]);
201
177
  }
202
- // ==========================================================================
203
- // APISERVICE OVERRIDES (Customize behavior)
204
- // ==========================================================================
205
- /**
206
- * Override: Convert DTO to entity (hash password before saving)
207
- */ async convertSingleDtoToEntity(dto, user) {
178
+ // ApiService Overrides
179
+ /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
208
180
  // Repository initialization is handled automatically by RequestScopedApiService
209
181
  const entity = await super.convertSingleDtoToEntity(dto, user);
210
- // Hash password if provided
211
- if (dto.password) {
212
- entity.password = await _bcrypt.hash(dto.password, 10);
182
+ // Hash password if provided (CreateUserDto has password field)
183
+ if ('password' in dto && dto.password) {
184
+ entity.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
213
185
  }
214
186
  return entity;
215
187
  }
216
- /**
217
- * Override: Convert entity to response DTO (remove password!)
218
- *
219
- * When company feature is enabled and raw mode is used, isActive comes from
220
- * the permission table (user_companyIsActive). Otherwise, it comes from User entity.
221
- */ convertEntityToResponseDto(entity, isRaw) {
188
+ /** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
189
+ if (isRaw) {
190
+ return this.convertRawToResponseDto(entity);
191
+ }
192
+ return this.convertEntityToDto(entity);
193
+ }
194
+ /** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
222
195
  // When raw mode is used with company feature, isActive comes from computed field
223
- const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : entity.isActive;
196
+ const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
224
197
  return {
225
- id: isRaw ? entity.user_id : entity.id,
226
- name: isRaw ? entity.user_name : entity.name,
227
- phone: isRaw ? entity.user_phone : entity.phone,
228
- email: isRaw ? entity.user_email : entity.email,
229
- profilePictureId: isRaw ? entity.user_profilePictureId : entity.profilePictureId,
198
+ id: raw.user_id,
199
+ name: raw.user_name,
200
+ phone: raw.user_phone,
201
+ email: raw.user_email,
202
+ profilePictureId: raw.user_profile_picture_id,
230
203
  isActive,
231
- emailVerified: isRaw ? entity.user_emailVerified : entity.emailVerified,
232
- phoneVerified: isRaw ? entity.user_phoneVerified : entity.phoneVerified,
233
- lastLoginAt: isRaw ? entity.user_lastLoginAt : entity.lastLoginAt,
234
- additionalFields: isRaw ? entity.user_additionalFields : entity.additionalFields,
235
- createdAt: isRaw ? entity.user_createdAt : entity.createdAt,
236
- updatedAt: isRaw ? entity.user_updatedAt : entity.updatedAt,
237
- deletedAt: isRaw ? entity.user_deletedAt : entity.deletedAt,
238
- createdById: isRaw ? entity.user_createdById : entity.createdById,
239
- updatedById: isRaw ? entity.user_updatedById : entity.updatedById,
240
- deletedById: isRaw ? entity.user_deletedById : entity.deletedById
204
+ emailVerified: raw.user_email_verified,
205
+ phoneVerified: raw.user_phone_verified,
206
+ lastLoginAt: raw.user_last_login_at,
207
+ additionalFields: raw.user_additional_fields,
208
+ createdAt: raw.user_created_at,
209
+ updatedAt: raw.user_updated_at,
210
+ deletedAt: raw.user_deleted_at,
211
+ createdById: raw.user_created_by_id,
212
+ updatedById: raw.user_updated_by_id,
213
+ deletedById: raw.user_deleted_by_id
241
214
  };
242
215
  }
243
- /**
244
- * Override: Global search - search by name, email, or phone
245
- */ async getGlobalSearchQuery(query, search, _user) {
216
+ /** Convert entity to response DTO */ convertEntityToDto(entity) {
217
+ return {
218
+ id: entity.id,
219
+ name: entity.name,
220
+ phone: entity.phone,
221
+ email: entity.email,
222
+ profilePictureId: entity.profilePictureId,
223
+ isActive: entity.isActive,
224
+ emailVerified: entity.emailVerified,
225
+ phoneVerified: entity.phoneVerified,
226
+ lastLoginAt: entity.lastLoginAt,
227
+ additionalFields: entity.additionalFields,
228
+ createdAt: entity.createdAt,
229
+ updatedAt: entity.updatedAt,
230
+ deletedAt: entity.deletedAt,
231
+ createdById: entity.createdById,
232
+ updatedById: entity.updatedById,
233
+ deletedById: entity.deletedById
234
+ };
235
+ }
236
+ /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
237
+ return entities.map((entity)=>this.convertEntityToResponseDto(entity, isRaw));
238
+ }
239
+ /** Global search - search by name, email, or phone */ async getGlobalSearchQuery(query, search, _user) {
246
240
  query.andWhere('(user.name LIKE :search OR user.email LIKE :search OR user.phone LIKE :search)', {
247
241
  search: `%${search}%`
248
242
  });
@@ -251,18 +245,13 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
251
245
  isRaw: false
252
246
  };
253
247
  }
254
- /**
255
- * Override: getExtraManipulateQuery - Add company permission-based isActive check when company feature enabled
256
- *
257
- * Note: When company feature is disabled at bootstrap, this method returns without modifications.
258
- * All company-related filtering is skipped, and users are returned normally with isActive from User entity.
259
- *
260
- * When company feature is enabled:
261
- * - isActive is computed from UserCompanyPermission table
262
- * - Returns isRaw: true to use raw query results
263
- */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
248
+ /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
264
249
  // Repository initialization is handled automatically by RequestScopedApiService
265
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
250
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
251
+ // Apply user enricher query modifications if available
252
+ if (this.userEnricher?.enrichListQuery) {
253
+ await this.userEnricher.enrichListQuery(query, user);
254
+ }
266
255
  // Only apply company filtering if:
267
256
  // 1. Company feature is enabled at bootstrap
268
257
  // 2. User has company context
@@ -315,12 +304,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
315
304
  }
316
305
  return result;
317
306
  }
318
- /**
319
- * Override: insert - Create company permission when company feature is enabled
320
- *
321
- * Note: When company feature is disabled at bootstrap, this method falls back to parent implementation.
322
- * No company permissions are created, and user is inserted normally.
323
- */ async insert(dto, user) {
307
+ /** Insert user. Creates company permission when company feature is enabled. */ async insert(dto, user) {
324
308
  // Repository initialization is handled automatically by RequestScopedApiService
325
309
  // Check if company feature is enabled
326
310
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -388,17 +372,143 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
388
372
  }
389
373
  }
390
374
  /**
391
- * Update user profile with password handling
392
- */ async profile(dto, user) {
393
- // Repository initialization is handled automatically by RequestScopedApiService
375
+ * Get user profile with extras from enricher
376
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
377
+ */ async getProfileWithExtras(userId, user) {
378
+ this.validateProfileOwnership(userId, user);
379
+ await this.ensureRepositoryInitialized();
380
+ const userEntity = await this.repository.findOne({
381
+ where: {
382
+ id: userId
383
+ }
384
+ });
385
+ if (!userEntity) {
386
+ throw new _common.NotFoundException('User not found');
387
+ }
388
+ const userData = this.convertEntityToResponseDto(userEntity, false);
389
+ // Get extras from enricher if available
390
+ let extras;
391
+ if (this.userEnricher?.getProfileExtras) {
392
+ extras = await this.userEnricher.getProfileExtras(userId, user);
393
+ }
394
+ return {
395
+ user: userData,
396
+ extras
397
+ };
398
+ }
399
+ /**
400
+ * Get enriched user list
401
+ * Applies enricher transformations after query execution
402
+ */ async getEnrichedList(users, user) {
403
+ if (this.userEnricher?.enrichListItems) {
404
+ return this.userEnricher.enrichListItems(users, user);
405
+ }
406
+ return users;
407
+ }
408
+ // ============================================
409
+ // Multi-Step Profile Methods (via Enricher)
410
+ // ============================================
411
+ /**
412
+ * Get profile section definitions from enricher
413
+ */ async getProfileSections() {
414
+ if (this.userEnricher?.getProfileSections) {
415
+ return this.userEnricher.getProfileSections();
416
+ }
417
+ return [];
418
+ }
419
+ /**
420
+ * Get specific profile section data
421
+ */ async getProfileSectionData(userId, sectionId, user) {
422
+ this.validateProfileOwnership(userId, user);
423
+ await this.ensureRepositoryInitialized();
424
+ // Verify user exists
425
+ const userEntity = await this.repository.findOne({
426
+ where: {
427
+ id: userId
428
+ }
429
+ });
430
+ if (!userEntity) {
431
+ throw new _common.NotFoundException('User not found');
432
+ }
433
+ if (this.userEnricher?.getProfileSectionData) {
434
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
435
+ }
436
+ return null;
437
+ }
438
+ /**
439
+ * Update specific profile section
440
+ */ async updateProfileSection(userId, sectionId, data, user) {
441
+ this.validateProfileOwnership(userId, user);
442
+ await this.ensureRepositoryInitialized();
394
443
  if (!this.dataSource) {
395
444
  throw new _common.BadRequestException('DataSource not available');
396
445
  }
446
+ // Verify user exists
447
+ const userEntity = await this.repository.findOne({
448
+ where: {
449
+ id: userId
450
+ }
451
+ });
452
+ if (!userEntity) {
453
+ throw new _common.NotFoundException('User not found');
454
+ }
455
+ if (!this.userEnricher?.updateProfileSection) {
456
+ throw new _common.BadRequestException('Profile sections not supported');
457
+ }
458
+ const queryRunner = this.dataSource.createQueryRunner();
459
+ await queryRunner.connect();
460
+ await queryRunner.startTransaction();
461
+ try {
462
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
463
+ await queryRunner.commitTransaction();
464
+ await this.clearCacheForId([
465
+ userEntity
466
+ ]);
467
+ } catch (error) {
468
+ await queryRunner.rollbackTransaction();
469
+ _utils.ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
470
+ entity: this.entityName,
471
+ sectionId
472
+ });
473
+ _utils.ErrorHandler.rethrowError(error);
474
+ } finally{
475
+ await queryRunner.release();
476
+ }
477
+ }
478
+ /**
479
+ * Get profile completion percentage
480
+ */ async getProfileCompletion(userId, user) {
481
+ this.validateProfileOwnership(userId, user);
482
+ await this.ensureRepositoryInitialized();
483
+ // Verify user exists
484
+ const userEntity = await this.repository.findOne({
485
+ where: {
486
+ id: userId
487
+ }
488
+ });
489
+ if (!userEntity) {
490
+ throw new _common.NotFoundException('User not found');
491
+ }
492
+ if (this.userEnricher?.calculateProfileCompletion) {
493
+ return this.userEnricher.calculateProfileCompletion(userId, user);
494
+ }
495
+ return 100; // Default to 100% if no enricher
496
+ }
497
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
498
+ // Ensure repository and dataSource are initialized
499
+ await this.ensureRepositoryInitialized();
500
+ if (!this.dataSource) {
501
+ throw new _common.BadRequestException('DataSource not available');
502
+ }
503
+ const userId = dto.id ?? user.id;
504
+ // Validate extras if enricher provides validation
505
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
506
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
507
+ }
397
508
  const queryRunner = this.dataSource.createQueryRunner();
398
509
  await queryRunner.connect();
399
510
  await queryRunner.startTransaction();
400
511
  try {
401
- const userId = dto.id ?? user.id;
402
512
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
403
513
  where: {
404
514
  id: userId
@@ -409,18 +519,28 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
409
519
  }
410
520
  // Handle password update
411
521
  if (dto.password) {
412
- // If updating own profile, require old password
413
- if (!dto.id || dto.id === user.id) {
414
- // This would need oldPassword in DTO - for now, skip if not provided
415
- // In real implementation, add oldPassword to UpdateUserDto
522
+ // If updating own profile, require old password verification
523
+ const isOwnProfile = !dto.id || dto.id === user.id;
524
+ if (isOwnProfile) {
525
+ if (!dto.oldPassword) {
526
+ throw new _common.BadRequestException('Current password is required to change your password');
527
+ }
528
+ const isPasswordValid = await _bcrypt.compare(dto.oldPassword, findUser.password);
529
+ if (!isPasswordValid) {
530
+ throw new _common.BadRequestException('Current password is incorrect');
531
+ }
416
532
  }
417
- const salt = await _bcrypt.genSalt();
418
- findUser.password = await _bcrypt.hash(dto.password, salt);
533
+ findUser.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
419
534
  }
420
- // Merge other fields (excluding password)
421
- const { password, ...restDto } = dto;
535
+ // Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
536
+ // Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
537
+ const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
422
538
  Object.assign(findUser, restDto);
423
539
  await queryRunner.manager.save(this.repository.target, findUser);
540
+ // Handle extras update via enricher
541
+ if (extras && this.userEnricher?.updateProfileExtras) {
542
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
543
+ }
424
544
  await queryRunner.commitTransaction();
425
545
  await this.clearCacheForId([
426
546
  findUser
@@ -436,10 +556,8 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
436
556
  await queryRunner.release();
437
557
  }
438
558
  }
439
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
440
- constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
441
- // Repository will be set asynchronously by RequestScopedApiService
442
- 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 _common.Logger(UserService.name);
559
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
560
+ 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;
443
561
  }
444
562
  };
445
563
  UserService = _ts_decorate([
@@ -450,11 +568,14 @@ UserService = _ts_decorate([
450
568
  _ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
451
569
  _ts_param(2, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
452
570
  _ts_param(3, (0, _common.Inject)(_authconfigservice.AuthConfigService)),
571
+ _ts_param(4, (0, _common.Optional)()),
572
+ _ts_param(4, (0, _common.Inject)(_config.USER_ENRICHER)),
453
573
  _ts_metadata("design:type", Function),
454
574
  _ts_metadata("design:paramtypes", [
455
575
  typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
456
576
  typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
457
577
  typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider,
458
- typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService
578
+ typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService,
579
+ typeof _userenricherinterface.IUserEnricher === "undefined" ? Object : _userenricherinterface.IUserEnricher
459
580
  ])
460
581
  ], UserService);
@@ -14,6 +14,7 @@ const _passport = require("@nestjs/passport");
14
14
  const _passportjwt = require("passport-jwt");
15
15
  const _authconfigservice = require("../services/auth-config.service");
16
16
  const _authdatasourceprovider = require("../services/auth-datasource.provider");
17
+ const _authenticationservice = require("../services/authentication.service");
17
18
  const _userentity = require("../entities/user.entity");
18
19
  function _define_property(obj, key, value) {
19
20
  if (key in obj) {
@@ -44,8 +45,9 @@ function _ts_metadata(k, v) {
44
45
  }
45
46
  let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_passportjwt.Strategy, 'jwt') {
46
47
  async validate(_req, payload) {
47
- // Resolve request-scoped AuthDataSourceProvider for this request
48
+ // Resolve request-scoped services for this request
48
49
  let userRepository = null;
50
+ let authService = null;
49
51
  try {
50
52
  const dataSourceProvider = await this.moduleRef.resolve(_authdatasourceprovider.AuthDataSourceProvider, undefined, {
51
53
  strict: false
@@ -53,12 +55,23 @@ let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_pas
53
55
  if (dataSourceProvider) {
54
56
  userRepository = await dataSourceProvider.getRepository(_userentity.User);
55
57
  }
58
+ // Resolve AuthenticationService for token revocation check
59
+ authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
60
+ strict: false
61
+ });
56
62
  } catch (error) {
57
63
  throw new _common.UnauthorizedException('Unable to access user data');
58
64
  }
59
65
  if (!userRepository) {
60
66
  throw new _common.UnauthorizedException('User repository not available');
61
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) {
72
+ throw new _common.UnauthorizedException('Token has been revoked');
73
+ }
74
+ }
62
75
  // Find user and check if active
63
76
  const user = await userRepository.findOne({
64
77
  where: {
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "IsStrongPassword", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return IsStrongPassword;
9
+ }
10
+ });
11
+ const _classvalidator = require("class-validator");
12
+ function IsStrongPassword(options = {}, validationOptions) {
13
+ const { minLength = 8, maxLength = 128, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = true } = options;
14
+ return function(object, propertyName) {
15
+ (0, _classvalidator.registerDecorator)({
16
+ name: 'isStrongPassword',
17
+ target: object.constructor,
18
+ propertyName: propertyName,
19
+ options: validationOptions,
20
+ validator: {
21
+ validate (value) {
22
+ if (typeof value !== 'string') return false;
23
+ const checks = [
24
+ value.length >= minLength,
25
+ value.length <= maxLength,
26
+ !requireUppercase || /[A-Z]/.test(value),
27
+ !requireLowercase || /[a-z]/.test(value),
28
+ !requireNumber || /[0-9]/.test(value),
29
+ !requireSpecialChar || /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(value)
30
+ ];
31
+ return checks.every(Boolean);
32
+ },
33
+ defaultMessage (args) {
34
+ const requirements = [];
35
+ if (minLength > 0) requirements.push(`at least ${minLength} characters`);
36
+ if (requireUppercase) requirements.push('one uppercase letter');
37
+ if (requireLowercase) requirements.push('one lowercase letter');
38
+ if (requireNumber) requirements.push('one number');
39
+ if (requireSpecialChar) requirements.push('one special character (!@#$%^&*...)');
40
+ return `Password must contain ${requirements.join(', ')}`;
41
+ }
42
+ }
43
+ });
44
+ };
45
+ }
@@ -1,2 +1,7 @@
1
1
  export declare const AUTH_MODULE_OPTIONS = "AUTH_MODULE_OPTIONS";
2
+ export declare const USER_ENRICHER = "USER_ENRICHER";
3
+ export declare const BCRYPT_SALT_ROUNDS = 12;
4
+ export declare const DEFAULT_COOKIE_NAME = "fsn_refresh_token";
5
+ export declare const MAX_COOKIE_BYTES = 3500;
2
6
  export declare const SESSION_CACHE_PREFIX = "auth:session";
7
+ export declare const TOKEN_REVOKED_PREFIX = "auth:revoked";
@@ -1,22 +1,26 @@
1
1
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
2
2
  import { Request } from 'express';
3
- import { ChangePasswordDto, CompanySelectionResponseDto, LoginDto, LoginResponseDto, MeResponseDto, RefreshTokenDto, RegistrationDto, RegistrationResponseDto } from '../dtos/authentication.dto';
3
+ import { ChangePasswordDto, CompanySelectionResponseDto, ForgotPasswordDto, LoginDto, LoginResponseDto, MeResponseDto, RefreshTokenDto, RegistrationDto, RegistrationResponseDto, ResendVerificationDto, ResetPasswordDto, VerifyEmailDto } from '../dtos/authentication.dto';
4
+ import { MessageResponseDto } from '@flusys/nestjs-shared/dtos';
4
5
  import { AuthConfigService } from '../services/auth-config.service';
6
+ import { AuthEmailService } from '../services/auth-email.service';
5
7
  import { AuthenticationService } from '../services/authentication.service';
6
8
  export declare class AuthenticationController {
7
9
  private readonly authService;
8
10
  private readonly authConfig?;
9
- constructor(authService: AuthenticationService, authConfig?: AuthConfigService);
11
+ private readonly authEmailService?;
12
+ constructor(authService: AuthenticationService, authConfig?: AuthConfigService, authEmailService?: AuthEmailService);
13
+ private buildAuthUrl;
10
14
  login(loginDto: LoginDto): Promise<LoginResponseDto | CompanySelectionResponseDto>;
11
- register(registrationDto: RegistrationDto): Promise<RegistrationResponseDto>;
15
+ register(req: Request, registrationDto: RegistrationDto): Promise<RegistrationResponseDto>;
12
16
  refreshToken(req: Request, refreshTokenDto: RefreshTokenDto): Promise<{
13
17
  accessToken: string;
14
18
  }>;
15
19
  getCurrentUser(user: ILoggedUserInfo): Promise<MeResponseDto>;
16
- logout(userId: string): Promise<{
17
- message: string;
18
- }>;
19
- changePassword(userId: string, dto: ChangePasswordDto): Promise<{
20
- message: string;
21
- }>;
20
+ logout(userId: string): Promise<MessageResponseDto>;
21
+ changePassword(userId: string, dto: ChangePasswordDto): Promise<MessageResponseDto>;
22
+ forgotPassword(req: Request, dto: ForgotPasswordDto): Promise<MessageResponseDto>;
23
+ resetPassword(dto: ResetPasswordDto): Promise<MessageResponseDto>;
24
+ resendVerification(req: Request, dto: ResendVerificationDto): Promise<MessageResponseDto>;
25
+ verifyEmail(dto: VerifyEmailDto): Promise<MessageResponseDto | RegistrationResponseDto>;
22
26
  }
@@ -6,9 +6,13 @@ export declare class CompanySelectionController {
6
6
  select(dto: SelectDto): Promise<LoginResponseDto>;
7
7
  switchCompany(userId: string, dto: SwitchCompanyDto): Promise<LoginResponseDto>;
8
8
  getUserCompanies(userId: string): Promise<{
9
- companies: import("..").Company[];
9
+ success: boolean;
10
+ message: string;
11
+ data: import("..").Company[];
10
12
  }>;
11
13
  getUserBranches(userId: string, companyId: string): Promise<{
12
- branches: import("..").CompanyBranch[];
14
+ success: boolean;
15
+ message: string;
16
+ data: import("..").CompanyBranch[];
13
17
  }>;
14
18
  }
@@ -1,5 +1,6 @@
1
1
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
2
2
  import { CreateUserDto, UpdateUserDto, UserResponseDto } from '../dtos/user.dto';
3
+ import { IUser } from '../interfaces/user.interface';
3
4
  import { UserService } from '../services/user.service';
4
5
  declare const UserController_base: abstract new (service: UserService) => {
5
6
  service: UserService;
@@ -14,18 +15,50 @@ declare const UserController_base: abstract new (service: UserService) => {
14
15
  export declare class UserController extends UserController_base {
15
16
  service: UserService;
16
17
  constructor(service: UserService);
18
+ getProfileWithExtras(id: string, user: ILoggedUserInfo): Promise<{
19
+ success: boolean;
20
+ data: {
21
+ user: IUser;
22
+ extras?: import("..").IProfileExtras;
23
+ };
24
+ }>;
17
25
  verifyEmail(id: string): Promise<{
26
+ success: boolean;
18
27
  message: string;
19
28
  }>;
20
29
  verifyPhone(id: string): Promise<{
30
+ success: boolean;
21
31
  message: string;
22
32
  }>;
23
33
  updateStatus(id: string, isActive: boolean, user: ILoggedUserInfo): Promise<{
34
+ success: boolean;
24
35
  message: string;
25
36
  }>;
26
37
  profile(dto: UpdateUserDto, user: ILoggedUserInfo): Promise<{
38
+ success: boolean;
27
39
  message: string;
28
40
  data: string;
29
41
  }>;
42
+ getProfileSections(): Promise<{
43
+ success: boolean;
44
+ message: string;
45
+ data: import("..").IProfileSection[];
46
+ }>;
47
+ getProfileSectionData(id: string, sectionId: string, user: ILoggedUserInfo): Promise<{
48
+ success: boolean;
49
+ message: string;
50
+ data: import("..").IProfileSectionData;
51
+ }>;
52
+ updateProfileSection(id: string, sectionId: string, data: Record<string, any>, user: ILoggedUserInfo): Promise<{
53
+ success: boolean;
54
+ message: string;
55
+ }>;
56
+ getProfileCompletion(id: string, user: ILoggedUserInfo): Promise<{
57
+ success: boolean;
58
+ message: string;
59
+ data: {
60
+ completionPercentage: number;
61
+ };
62
+ }>;
30
63
  }
31
64
  export {};