@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
@@ -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, ForbiddenException, 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,16 @@ export class UserService extends RequestScopedApiService {
65
58
  this.dataSource = await this.getDataSourceForService();
66
59
  }
67
60
  }
68
- // ==========================================================================
69
- // CUSTOM METHODS (Beyond RequestScopedApiService)
70
- // ==========================================================================
61
+ // Custom Methods
71
62
  /**
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
- ]);
63
+ * Validate that the requesting user owns the target profile.
64
+ * Users can only access/modify their own profile data.
65
+ */ validateProfileOwnership(targetUserId, requestingUser) {
66
+ if (targetUserId !== requestingUser.id) {
67
+ throw new ForbiddenException('Cannot access another user\'s profile');
68
+ }
84
69
  }
85
- /**
86
- * Verify email
87
- */ async verifyEmail(id) {
70
+ /** Verify email */ async verifyEmail(id) {
88
71
  // Ensure repository is initialized for custom method
89
72
  await this.ensureRepositoryInitialized();
90
73
  await this.repository.update(id, {
@@ -97,9 +80,7 @@ export class UserService extends RequestScopedApiService {
97
80
  }
98
81
  ]);
99
82
  }
100
- /**
101
- * Verify phone
102
- */ async verifyPhone(id) {
83
+ /** Verify phone */ async verifyPhone(id) {
103
84
  // Ensure repository is initialized for custom method
104
85
  await this.ensureRepositoryInitialized();
105
86
  await this.repository.update(id, {
@@ -112,12 +93,7 @@ export class UserService extends RequestScopedApiService {
112
93
  }
113
94
  ]);
114
95
  }
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) {
96
+ /** Update user status. When company feature enabled, updates permission isActive for logged user's company. */ async updateStatus(id, isActive, user) {
121
97
  // Ensure repository is initialized for custom method
122
98
  await this.ensureRepositoryInitialized();
123
99
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -148,50 +124,68 @@ export class UserService extends RequestScopedApiService {
148
124
  }
149
125
  ]);
150
126
  }
151
- // ==========================================================================
152
- // APISERVICE OVERRIDES (Customize behavior)
153
- // ==========================================================================
154
- /**
155
- * Override: Convert DTO to entity (hash password before saving)
156
- */ async convertSingleDtoToEntity(dto, user) {
127
+ // ApiService Overrides
128
+ /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
157
129
  // Repository initialization is handled automatically by RequestScopedApiService
158
130
  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);
131
+ // Hash password if provided (CreateUserDto has password field)
132
+ if ('password' in dto && dto.password) {
133
+ entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
162
134
  }
163
135
  return entity;
164
136
  }
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) {
137
+ /** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
138
+ if (isRaw) {
139
+ return this.convertRawToResponseDto(entity);
140
+ }
141
+ return this.convertEntityToDto(entity);
142
+ }
143
+ /** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
171
144
  // 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;
145
+ const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
173
146
  return {
174
- id: isRaw ? entity.user_id : entity.id,
175
- name: isRaw ? entity.user_name : entity.name,
176
- phone: isRaw ? entity.user_phone : entity.phone,
177
- email: isRaw ? entity.user_email : entity.email,
178
- profilePictureId: isRaw ? entity.user_profilePictureId : entity.profilePictureId,
147
+ id: raw.user_id,
148
+ name: raw.user_name,
149
+ phone: raw.user_phone,
150
+ email: raw.user_email,
151
+ profilePictureId: raw.user_profile_picture_id,
179
152
  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
153
+ emailVerified: raw.user_email_verified,
154
+ phoneVerified: raw.user_phone_verified,
155
+ lastLoginAt: raw.user_last_login_at,
156
+ additionalFields: raw.user_additional_fields,
157
+ createdAt: raw.user_created_at,
158
+ updatedAt: raw.user_updated_at,
159
+ deletedAt: raw.user_deleted_at,
160
+ createdById: raw.user_created_by_id,
161
+ updatedById: raw.user_updated_by_id,
162
+ deletedById: raw.user_deleted_by_id
190
163
  };
191
164
  }
192
- /**
193
- * Override: Global search - search by name, email, or phone
194
- */ async getGlobalSearchQuery(query, search, _user) {
165
+ /** Convert entity to response DTO */ convertEntityToDto(entity) {
166
+ return {
167
+ id: entity.id,
168
+ name: entity.name,
169
+ phone: entity.phone,
170
+ email: entity.email,
171
+ profilePictureId: entity.profilePictureId,
172
+ isActive: entity.isActive,
173
+ emailVerified: entity.emailVerified,
174
+ phoneVerified: entity.phoneVerified,
175
+ lastLoginAt: entity.lastLoginAt,
176
+ additionalFields: entity.additionalFields,
177
+ createdAt: entity.createdAt,
178
+ updatedAt: entity.updatedAt,
179
+ deletedAt: entity.deletedAt,
180
+ createdById: entity.createdById,
181
+ updatedById: entity.updatedById,
182
+ deletedById: entity.deletedById
183
+ };
184
+ }
185
+ /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
186
+ return entities.map((entity)=>this.convertEntityToResponseDto(entity, isRaw));
187
+ }
188
+ /** Global search - search by name, email, or phone */ async getGlobalSearchQuery(query, search, _user) {
195
189
  query.andWhere('(user.name LIKE :search OR user.email LIKE :search OR user.phone LIKE :search)', {
196
190
  search: `%${search}%`
197
191
  });
@@ -200,18 +194,13 @@ export class UserService extends RequestScopedApiService {
200
194
  isRaw: false
201
195
  };
202
196
  }
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) {
197
+ /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
213
198
  // Repository initialization is handled automatically by RequestScopedApiService
214
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
199
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
200
+ // Apply user enricher query modifications if available
201
+ if (this.userEnricher?.enrichListQuery) {
202
+ await this.userEnricher.enrichListQuery(query, user);
203
+ }
215
204
  // Only apply company filtering if:
216
205
  // 1. Company feature is enabled at bootstrap
217
206
  // 2. User has company context
@@ -264,12 +253,7 @@ export class UserService extends RequestScopedApiService {
264
253
  }
265
254
  return result;
266
255
  }
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) {
256
+ /** Insert user. Creates company permission when company feature is enabled. */ async insert(dto, user) {
273
257
  // Repository initialization is handled automatically by RequestScopedApiService
274
258
  // Check if company feature is enabled
275
259
  const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
@@ -337,17 +321,143 @@ export class UserService extends RequestScopedApiService {
337
321
  }
338
322
  }
339
323
  /**
340
- * Update user profile with password handling
341
- */ async profile(dto, user) {
342
- // Repository initialization is handled automatically by RequestScopedApiService
324
+ * Get user profile with extras from enricher
325
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
326
+ */ async getProfileWithExtras(userId, user) {
327
+ this.validateProfileOwnership(userId, user);
328
+ await this.ensureRepositoryInitialized();
329
+ const userEntity = await this.repository.findOne({
330
+ where: {
331
+ id: userId
332
+ }
333
+ });
334
+ if (!userEntity) {
335
+ throw new NotFoundException('User not found');
336
+ }
337
+ const userData = this.convertEntityToResponseDto(userEntity, false);
338
+ // Get extras from enricher if available
339
+ let extras;
340
+ if (this.userEnricher?.getProfileExtras) {
341
+ extras = await this.userEnricher.getProfileExtras(userId, user);
342
+ }
343
+ return {
344
+ user: userData,
345
+ extras
346
+ };
347
+ }
348
+ /**
349
+ * Get enriched user list
350
+ * Applies enricher transformations after query execution
351
+ */ async getEnrichedList(users, user) {
352
+ if (this.userEnricher?.enrichListItems) {
353
+ return this.userEnricher.enrichListItems(users, user);
354
+ }
355
+ return users;
356
+ }
357
+ // ============================================
358
+ // Multi-Step Profile Methods (via Enricher)
359
+ // ============================================
360
+ /**
361
+ * Get profile section definitions from enricher
362
+ */ async getProfileSections() {
363
+ if (this.userEnricher?.getProfileSections) {
364
+ return this.userEnricher.getProfileSections();
365
+ }
366
+ return [];
367
+ }
368
+ /**
369
+ * Get specific profile section data
370
+ */ async getProfileSectionData(userId, sectionId, user) {
371
+ this.validateProfileOwnership(userId, user);
372
+ await this.ensureRepositoryInitialized();
373
+ // Verify user exists
374
+ const userEntity = await this.repository.findOne({
375
+ where: {
376
+ id: userId
377
+ }
378
+ });
379
+ if (!userEntity) {
380
+ throw new NotFoundException('User not found');
381
+ }
382
+ if (this.userEnricher?.getProfileSectionData) {
383
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
384
+ }
385
+ return null;
386
+ }
387
+ /**
388
+ * Update specific profile section
389
+ */ async updateProfileSection(userId, sectionId, data, user) {
390
+ this.validateProfileOwnership(userId, user);
391
+ await this.ensureRepositoryInitialized();
343
392
  if (!this.dataSource) {
344
393
  throw new BadRequestException('DataSource not available');
345
394
  }
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?.updateProfileSection) {
405
+ throw new BadRequestException('Profile sections not supported');
406
+ }
407
+ const queryRunner = this.dataSource.createQueryRunner();
408
+ await queryRunner.connect();
409
+ await queryRunner.startTransaction();
410
+ try {
411
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
412
+ await queryRunner.commitTransaction();
413
+ await this.clearCacheForId([
414
+ userEntity
415
+ ]);
416
+ } catch (error) {
417
+ await queryRunner.rollbackTransaction();
418
+ ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
419
+ entity: this.entityName,
420
+ sectionId
421
+ });
422
+ ErrorHandler.rethrowError(error);
423
+ } finally{
424
+ await queryRunner.release();
425
+ }
426
+ }
427
+ /**
428
+ * Get profile completion percentage
429
+ */ async getProfileCompletion(userId, user) {
430
+ this.validateProfileOwnership(userId, user);
431
+ await this.ensureRepositoryInitialized();
432
+ // Verify user exists
433
+ const userEntity = await this.repository.findOne({
434
+ where: {
435
+ id: userId
436
+ }
437
+ });
438
+ if (!userEntity) {
439
+ throw new NotFoundException('User not found');
440
+ }
441
+ if (this.userEnricher?.calculateProfileCompletion) {
442
+ return this.userEnricher.calculateProfileCompletion(userId, user);
443
+ }
444
+ return 100; // Default to 100% if no enricher
445
+ }
446
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
447
+ // Ensure repository and dataSource are initialized
448
+ await this.ensureRepositoryInitialized();
449
+ if (!this.dataSource) {
450
+ throw new BadRequestException('DataSource not available');
451
+ }
452
+ const userId = dto.id ?? user.id;
453
+ // Validate extras if enricher provides validation
454
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
455
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
456
+ }
346
457
  const queryRunner = this.dataSource.createQueryRunner();
347
458
  await queryRunner.connect();
348
459
  await queryRunner.startTransaction();
349
460
  try {
350
- const userId = dto.id ?? user.id;
351
461
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
352
462
  where: {
353
463
  id: userId
@@ -358,18 +468,28 @@ export class UserService extends RequestScopedApiService {
358
468
  }
359
469
  // Handle password update
360
470
  if (dto.password) {
361
- // If updating own profile, require old password
362
- if (!dto.id || dto.id === user.id) {
363
- // This would need oldPassword in DTO - for now, skip if not provided
364
- // In real implementation, add oldPassword to UpdateUserDto
471
+ // If updating own profile, require old password verification
472
+ const isOwnProfile = !dto.id || dto.id === user.id;
473
+ if (isOwnProfile) {
474
+ if (!dto.oldPassword) {
475
+ throw new BadRequestException('Current password is required to change your password');
476
+ }
477
+ const isPasswordValid = await bcrypt.compare(dto.oldPassword, findUser.password);
478
+ if (!isPasswordValid) {
479
+ throw new BadRequestException('Current password is incorrect');
480
+ }
365
481
  }
366
- const salt = await bcrypt.genSalt();
367
- findUser.password = await bcrypt.hash(dto.password, salt);
482
+ findUser.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
368
483
  }
369
- // Merge other fields (excluding password)
370
- const { password, ...restDto } = dto;
484
+ // Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
485
+ // Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
486
+ const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
371
487
  Object.assign(findUser, restDto);
372
488
  await queryRunner.manager.save(this.repository.target, findUser);
489
+ // Handle extras update via enricher
490
+ if (extras && this.userEnricher?.updateProfileExtras) {
491
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
492
+ }
373
493
  await queryRunner.commitTransaction();
374
494
  await this.clearCacheForId([
375
495
  findUser
@@ -385,10 +505,8 @@ export class UserService extends RequestScopedApiService {
385
505
  await queryRunner.release();
386
506
  }
387
507
  }
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);
508
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
509
+ 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
510
  }
393
511
  }
394
512
  UserService = _ts_decorate([
@@ -399,11 +517,14 @@ UserService = _ts_decorate([
399
517
  _ts_param(1, Inject(UtilsService)),
400
518
  _ts_param(2, Inject(AuthDataSourceProvider)),
401
519
  _ts_param(3, Inject(AuthConfigService)),
520
+ _ts_param(4, Optional()),
521
+ _ts_param(4, Inject(USER_ENRICHER)),
402
522
  _ts_metadata("design:type", Function),
403
523
  _ts_metadata("design:paramtypes", [
404
524
  typeof HybridCache === "undefined" ? Object : HybridCache,
405
525
  typeof UtilsService === "undefined" ? Object : UtilsService,
406
526
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
407
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
527
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
528
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher
408
529
  ])
409
530
  ], UserService);
@@ -26,6 +26,7 @@ import { PassportStrategy } from '@nestjs/passport';
26
26
  import { ExtractJwt, Strategy } from 'passport-jwt';
27
27
  import { AuthConfigService } from '../services/auth-config.service';
28
28
  import { AuthDataSourceProvider } from '../services/auth-datasource.provider';
29
+ import { AuthenticationService } from '../services/authentication.service';
29
30
  import { User } from '../entities/user.entity';
30
31
  /**
31
32
  * Type guard to check if payload includes company context
@@ -34,8 +35,9 @@ import { User } from '../entities/user.entity';
34
35
  }
35
36
  export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
36
37
  async validate(_req, payload) {
37
- // Resolve request-scoped AuthDataSourceProvider for this request
38
+ // Resolve request-scoped services for this request
38
39
  let userRepository = null;
40
+ let authService = null;
39
41
  try {
40
42
  const dataSourceProvider = await this.moduleRef.resolve(AuthDataSourceProvider, undefined, {
41
43
  strict: false
@@ -43,12 +45,23 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
43
45
  if (dataSourceProvider) {
44
46
  userRepository = await dataSourceProvider.getRepository(User);
45
47
  }
48
+ // Resolve AuthenticationService for token revocation check
49
+ authService = await this.moduleRef.resolve(AuthenticationService, undefined, {
50
+ strict: false
51
+ });
46
52
  } catch (error) {
47
53
  throw new UnauthorizedException('Unable to access user data');
48
54
  }
49
55
  if (!userRepository) {
50
56
  throw new UnauthorizedException('User repository not available');
51
57
  }
58
+ // Check if token is revoked (logged out)
59
+ if (authService && payload.iat) {
60
+ const isRevoked = await authService.isTokenRevoked(payload.id, payload.iat);
61
+ if (isRevoked) {
62
+ throw new UnauthorizedException('Token has been revoked');
63
+ }
64
+ }
52
65
  // Find user and check if active
53
66
  const user = await userRepository.findOne({
54
67
  where: {
@@ -0,0 +1,48 @@
1
+ import { registerDecorator } from 'class-validator';
2
+ /**
3
+ * Strong password validation decorator.
4
+ * Validates that passwords meet complexity requirements to prevent weak passwords.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * @IsStrongPassword()
9
+ * password!: string;
10
+ *
11
+ * // With custom options
12
+ * @IsStrongPassword({ minLength: 12, requireSpecialChar: false })
13
+ * password!: string;
14
+ * ```
15
+ */ export function IsStrongPassword(options = {}, validationOptions) {
16
+ const { minLength = 8, maxLength = 128, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = true } = options;
17
+ return function(object, propertyName) {
18
+ registerDecorator({
19
+ name: 'isStrongPassword',
20
+ target: object.constructor,
21
+ propertyName: propertyName,
22
+ options: validationOptions,
23
+ validator: {
24
+ validate (value) {
25
+ if (typeof value !== 'string') return false;
26
+ const checks = [
27
+ value.length >= minLength,
28
+ value.length <= maxLength,
29
+ !requireUppercase || /[A-Z]/.test(value),
30
+ !requireLowercase || /[a-z]/.test(value),
31
+ !requireNumber || /[0-9]/.test(value),
32
+ !requireSpecialChar || /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(value)
33
+ ];
34
+ return checks.every(Boolean);
35
+ },
36
+ defaultMessage (args) {
37
+ const requirements = [];
38
+ if (minLength > 0) requirements.push(`at least ${minLength} characters`);
39
+ if (requireUppercase) requirements.push('one uppercase letter');
40
+ if (requireLowercase) requirements.push('one lowercase letter');
41
+ if (requireNumber) requirements.push('one number');
42
+ if (requireSpecialChar) requirements.push('one special character (!@#$%^&*...)');
43
+ return `Password must contain ${requirements.join(', ')}`;
44
+ }
45
+ }
46
+ });
47
+ };
48
+ }
@@ -0,0 +1,7 @@
1
+ import { ExecutionContext } from '@nestjs/common';
2
+ import { ThrottlerGuard } from '@nestjs/throttler';
3
+ export declare class AuthThrottlerGuard extends ThrottlerGuard {
4
+ protected getTracker(req: Record<string, unknown>): Promise<string>;
5
+ protected throwThrottlingException(): Promise<void>;
6
+ protected shouldSkip(context: ExecutionContext): Promise<boolean>;
7
+ }
@@ -5,6 +5,7 @@ export declare class SetToken implements NestInterceptor {
5
5
  private readonly authConfig?;
6
6
  constructor(authConfig?: AuthConfigService);
7
7
  private get cookieName();
8
+ private getMaxAgeMs;
8
9
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
9
10
  private isBrowserRequest;
10
11
  private setRefreshTokenCookie;
@@ -0,0 +1,6 @@
1
+ export interface IAuthEmailProvider {
2
+ sendPasswordResetEmail(email: string, token: string, resetUrl: string): Promise<void>;
3
+ sendVerificationEmail(email: string, token: string, verifyUrl: string): Promise<void>;
4
+ sendWelcomeEmail?(email: string, name: string): Promise<void>;
5
+ }
6
+ export declare const AUTH_EMAIL_PROVIDER = "AUTH_EMAIL_PROVIDER";
@@ -1,15 +1,17 @@
1
1
  import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
2
- import { ModuleMetadata, Type } from '@nestjs/common';
2
+ import { ModuleMetadata, Provider, Type } from '@nestjs/common';
3
3
  export interface IAuthModuleConfig extends IDataSourceServiceOptions {
4
4
  jwtSecret?: string;
5
5
  jwtExpiration?: string;
6
6
  refreshTokenSecret?: string;
7
7
  refreshTokenExpiration?: string;
8
8
  refreshTokenCookieName?: string;
9
+ frontendUrl?: string;
9
10
  }
10
11
  export interface AuthModuleOptions extends IDynamicModuleConfig {
11
12
  bootstrapAppConfig?: IBootstrapAppConfig;
12
13
  config?: IAuthModuleConfig;
14
+ providers?: Provider[];
13
15
  }
14
16
  export interface AuthOptionsFactory extends IModuleOptionsFactory<IAuthModuleConfig> {
15
17
  createAuthOptions(): Promise<IAuthModuleConfig> | IAuthModuleConfig;
@@ -21,4 +23,5 @@ export interface AuthModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>,
21
23
  inject?: any[];
22
24
  useClass?: Type<AuthOptionsFactory>;
23
25
  useExisting?: Type<AuthOptionsFactory>;
26
+ providers?: Provider[];
24
27
  }
@@ -51,3 +51,17 @@ export interface LoginResponseRequiresSelection {
51
51
  user: IUserMini;
52
52
  companies: CompanyWithBranches[];
53
53
  }
54
+ export interface RegistrationResponse {
55
+ accessToken?: string;
56
+ refreshToken?: string;
57
+ user: IUserMini;
58
+ company?: ICompanyMini;
59
+ branch?: ICompanyBranchMini;
60
+ companyFeatureEnabled?: boolean;
61
+ requiresEmailVerification?: boolean;
62
+ }
63
+ export interface LoginResponseRequiresEmailVerification {
64
+ requiresEmailVerification: true;
65
+ email: string;
66
+ message: string;
67
+ }
@@ -1,5 +1,7 @@
1
1
  export * from './authentication.interface';
2
+ export * from './auth-email-provider.interface';
2
3
  export * from './company-branch.interface';
3
4
  export * from './company.interface';
4
5
  export * from './user.interface';
6
+ export * from './user-enricher.interface';
5
7
  export * from './auth-module-options.interface';