@flusys/nestjs-auth 1.0.0-beta → 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 (95) hide show
  1. package/README.md +274 -291
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +219 -10
  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 +40 -25
  8. package/cjs/controllers/user.controller.js +211 -7
  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/index.js +2 -0
  21. package/cjs/interfaces/user-enricher.interface.js +4 -0
  22. package/cjs/modules/auth.module.js +8 -20
  23. package/cjs/services/auth-config.service.js +23 -2
  24. package/cjs/services/auth-datasource.provider.js +5 -3
  25. package/cjs/services/auth-email.service.js +471 -0
  26. package/cjs/services/authentication.service.js +169 -77
  27. package/cjs/services/branch.service.js +6 -5
  28. package/cjs/services/company-selection-session.service.js +1 -1
  29. package/cjs/services/index.js +1 -0
  30. package/cjs/services/user-permission.service.js +0 -2
  31. package/cjs/services/user.service.js +213 -34
  32. package/cjs/validators/password.validator.js +45 -0
  33. package/config/auth.constants.d.ts +3 -0
  34. package/controllers/authentication.controller.d.ts +13 -9
  35. package/controllers/company-selection.controller.d.ts +6 -2
  36. package/controllers/user.controller.d.ts +33 -0
  37. package/dtos/authentication.dto.d.ts +21 -3
  38. package/dtos/company-branch.dto.d.ts +0 -1
  39. package/dtos/company.dto.d.ts +0 -1
  40. package/dtos/user-permission.dto.d.ts +4 -0
  41. package/dtos/user.dto.d.ts +1 -2
  42. package/entities/index.d.ts +7 -3
  43. package/entities/pending-registration.entity.d.ts +17 -0
  44. package/entities/user-company-permission.entity.d.ts +0 -2
  45. package/entities/user-token.entity.d.ts +15 -0
  46. package/fesm/config/auth.constants.js +6 -2
  47. package/fesm/controllers/authentication.controller.js +221 -12
  48. package/fesm/controllers/branch.controller.js +45 -2
  49. package/fesm/controllers/company-selection.controller.js +28 -21
  50. package/fesm/controllers/company.controller.js +45 -2
  51. package/fesm/controllers/user-permission.controller.js +41 -26
  52. package/fesm/controllers/user.controller.js +214 -10
  53. package/fesm/dtos/authentication.dto.js +106 -14
  54. package/fesm/dtos/user-permission.dto.js +28 -0
  55. package/fesm/dtos/user.dto.js +20 -5
  56. package/fesm/entities/index.js +7 -1
  57. package/fesm/entities/pending-registration.entity.js +115 -0
  58. package/fesm/entities/user-company-permission.entity.js +0 -6
  59. package/fesm/entities/user-token.entity.js +113 -0
  60. package/fesm/guards/auth-throttler.guard.js +43 -0
  61. package/fesm/interceptors/clear-token.interceptor.js +2 -5
  62. package/fesm/interceptors/set-token.interceptor.js +37 -30
  63. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  64. package/fesm/interfaces/authentication.interface.js +1 -1
  65. package/fesm/interfaces/index.js +2 -0
  66. package/fesm/interfaces/user-enricher.interface.js +26 -0
  67. package/fesm/modules/auth.module.js +10 -22
  68. package/fesm/services/auth-config.service.js +24 -3
  69. package/fesm/services/auth-datasource.provider.js +6 -4
  70. package/fesm/services/auth-email.service.js +420 -0
  71. package/fesm/services/authentication.service.js +169 -77
  72. package/fesm/services/branch.service.js +6 -5
  73. package/fesm/services/company-selection-session.service.js +1 -1
  74. package/fesm/services/index.js +1 -0
  75. package/fesm/services/user-permission.service.js +1 -3
  76. package/fesm/services/user.service.js +215 -36
  77. package/fesm/validators/password.validator.js +48 -0
  78. package/guards/auth-throttler.guard.d.ts +7 -0
  79. package/interceptors/set-token.interceptor.d.ts +1 -0
  80. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  81. package/interfaces/auth-module-options.interface.d.ts +4 -1
  82. package/interfaces/authentication.interface.d.ts +14 -0
  83. package/interfaces/index.d.ts +2 -0
  84. package/interfaces/user-enricher.interface.d.ts +64 -0
  85. package/interfaces/user.interface.d.ts +1 -0
  86. package/modules/auth.module.d.ts +0 -1
  87. package/package.json +4 -4
  88. package/services/auth-config.service.d.ts +5 -1
  89. package/services/auth-email.service.d.ts +51 -0
  90. package/services/authentication.service.d.ts +16 -11
  91. package/services/company-selection-session.service.d.ts +1 -1
  92. package/services/index.d.ts +1 -0
  93. package/services/user-permission.service.d.ts +0 -1
  94. package/services/user.service.d.ts +19 -3
  95. package/validators/password.validator.d.ts +10 -0
@@ -28,12 +28,13 @@ 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, 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 } from '../config';
35
+ import { BCRYPT_SALT_ROUNDS, USER_ENRICHER } from '../config';
36
36
  import { PermissionType } from '../enums';
37
+ import { IUserEnricher } from '../interfaces/user-enricher.interface';
37
38
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
38
39
  import { User } from '../entities/user.entity';
39
40
  export class UserService extends RequestScopedApiService {
@@ -58,6 +59,14 @@ export class UserService extends RequestScopedApiService {
58
59
  }
59
60
  }
60
61
  // Custom Methods
62
+ /**
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
+ }
69
+ }
61
70
  /** Verify email */ async verifyEmail(id) {
62
71
  // Ensure repository is initialized for custom method
63
72
  await this.ensureRepositoryInitialized();
@@ -119,33 +128,58 @@ export class UserService extends RequestScopedApiService {
119
128
  /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
120
129
  // Repository initialization is handled automatically by RequestScopedApiService
121
130
  const entity = await super.convertSingleDtoToEntity(dto, user);
122
- // Hash password if provided
123
- if (dto.password) {
131
+ // Hash password if provided (CreateUserDto has password field)
132
+ if ('password' in dto && dto.password) {
124
133
  entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
125
134
  }
126
135
  return entity;
127
136
  }
128
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) {
129
144
  // When raw mode is used with company feature, isActive comes from computed field
130
- // Raw mode uses snake_case column names (due to SnakeNamingStrategy)
131
- const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : isRaw ? entity.user_is_active : entity.isActive;
145
+ const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
132
146
  return {
133
- id: isRaw ? entity.user_id : entity.id,
134
- name: isRaw ? entity.user_name : entity.name,
135
- phone: isRaw ? entity.user_phone : entity.phone,
136
- email: isRaw ? entity.user_email : entity.email,
137
- profilePictureId: isRaw ? entity.user_profile_picture_id : 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,
138
152
  isActive,
139
- emailVerified: isRaw ? entity.user_email_verified : entity.emailVerified,
140
- phoneVerified: isRaw ? entity.user_phone_verified : entity.phoneVerified,
141
- lastLoginAt: isRaw ? entity.user_last_login_at : entity.lastLoginAt,
142
- additionalFields: isRaw ? entity.user_additional_fields : entity.additionalFields,
143
- createdAt: isRaw ? entity.user_created_at : entity.createdAt,
144
- updatedAt: isRaw ? entity.user_updated_at : entity.updatedAt,
145
- deletedAt: isRaw ? entity.user_deleted_at : entity.deletedAt,
146
- createdById: isRaw ? entity.user_created_by_id : entity.createdById,
147
- updatedById: isRaw ? entity.user_updated_by_id : entity.updatedById,
148
- deletedById: isRaw ? entity.user_deleted_by_id : 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
163
+ };
164
+ }
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
149
183
  };
150
184
  }
151
185
  /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
@@ -162,7 +196,11 @@ export class UserService extends RequestScopedApiService {
162
196
  }
163
197
  /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
164
198
  // Repository initialization is handled automatically by RequestScopedApiService
165
- 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
+ }
166
204
  // Only apply company filtering if:
167
205
  // 1. Company feature is enabled at bootstrap
168
206
  // 2. User has company context
@@ -282,16 +320,144 @@ export class UserService extends RequestScopedApiService {
282
320
  await queryRunner.release();
283
321
  }
284
322
  }
285
- /** Update user profile with password handling */ async profile(dto, user) {
286
- // Repository initialization is handled automatically by RequestScopedApiService
323
+ /**
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();
287
392
  if (!this.dataSource) {
288
393
  throw new BadRequestException('DataSource not available');
289
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
+ }
290
457
  const queryRunner = this.dataSource.createQueryRunner();
291
458
  await queryRunner.connect();
292
459
  await queryRunner.startTransaction();
293
460
  try {
294
- const userId = dto.id ?? user.id;
295
461
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
296
462
  where: {
297
463
  id: userId
@@ -302,18 +468,28 @@ export class UserService extends RequestScopedApiService {
302
468
  }
303
469
  // Handle password update
304
470
  if (dto.password) {
305
- // If updating own profile, require old password
306
- if (!dto.id || dto.id === user.id) {
307
- // This would need oldPassword in DTO - for now, skip if not provided
308
- // 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
+ }
309
481
  }
310
- const salt = await bcrypt.genSalt();
311
- findUser.password = await bcrypt.hash(dto.password, salt);
482
+ findUser.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
312
483
  }
313
- // Merge other fields (excluding password)
314
- 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;
315
487
  Object.assign(findUser, restDto);
316
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
+ }
317
493
  await queryRunner.commitTransaction();
318
494
  await this.clearCacheForId([
319
495
  findUser
@@ -329,8 +505,8 @@ export class UserService extends RequestScopedApiService {
329
505
  await queryRunner.release();
330
506
  }
331
507
  }
332
- constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
333
- 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;
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;
334
510
  }
335
511
  }
336
512
  UserService = _ts_decorate([
@@ -341,11 +517,14 @@ UserService = _ts_decorate([
341
517
  _ts_param(1, Inject(UtilsService)),
342
518
  _ts_param(2, Inject(AuthDataSourceProvider)),
343
519
  _ts_param(3, Inject(AuthConfigService)),
520
+ _ts_param(4, Optional()),
521
+ _ts_param(4, Inject(USER_ENRICHER)),
344
522
  _ts_metadata("design:type", Function),
345
523
  _ts_metadata("design:paramtypes", [
346
524
  typeof HybridCache === "undefined" ? Object : HybridCache,
347
525
  typeof UtilsService === "undefined" ? Object : UtilsService,
348
526
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
349
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
527
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
528
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher
350
529
  ])
351
530
  ], UserService);
@@ -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';
@@ -0,0 +1,64 @@
1
+ import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
2
+ import { QueryRunner, SelectQueryBuilder } from 'typeorm';
3
+ import { User } from '../entities/user.entity';
4
+ import { IUser } from './user.interface';
5
+ export interface IProfileSection {
6
+ id: string;
7
+ name: string;
8
+ order: number;
9
+ icon?: string;
10
+ required?: boolean;
11
+ hasFileUpload?: boolean;
12
+ editPermission?: string;
13
+ }
14
+ export interface IProfileSectionData {
15
+ sectionId: string;
16
+ data: Record<string, any>;
17
+ isComplete: boolean;
18
+ files?: IProfileFile[];
19
+ }
20
+ export interface IProfileFile {
21
+ field: string;
22
+ id: string;
23
+ name: string;
24
+ url?: string;
25
+ type?: string;
26
+ }
27
+ export interface IProfileExtras {
28
+ roles?: IProfileRole[];
29
+ actions?: IProfileAction[];
30
+ sections?: IProfileSectionData[];
31
+ completionPercentage?: number;
32
+ }
33
+ export interface IProfileRole {
34
+ id: string;
35
+ name: string;
36
+ description?: string | null;
37
+ }
38
+ export interface IProfileAction {
39
+ id: string;
40
+ name: string;
41
+ code: string;
42
+ description?: string | null;
43
+ }
44
+ export type IEnrichedUser = IUser & IProfileExtras;
45
+ export interface IUserEnricher {
46
+ enrichListQuery?(query: SelectQueryBuilder<User>, user: ILoggedUserInfo | null): Promise<{
47
+ extraFields: string[];
48
+ }>;
49
+ enrichListItems?(users: IUser[], user: ILoggedUserInfo | null): Promise<IEnrichedUser[]>;
50
+ getProfileExtras?(userId: string, user: ILoggedUserInfo): Promise<IProfileExtras>;
51
+ updateProfileExtras?(userId: string, extras: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
52
+ validateProfileExtras?(userId: string, extras: Record<string, any>, user: ILoggedUserInfo): Promise<void>;
53
+ getProfileSections?(): IProfileSection[];
54
+ getProfileSectionData?(userId: string, sectionId: string, user: ILoggedUserInfo): Promise<IProfileSectionData>;
55
+ updateProfileSection?(userId: string, sectionId: string, data: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
56
+ handleSectionFileUpload?(userId: string, sectionId: string, field: string, fileInfo: {
57
+ name: string;
58
+ url: string;
59
+ size: number;
60
+ type: string;
61
+ }, queryRunner: QueryRunner): Promise<IProfileFile>;
62
+ handleSectionFileDelete?(userId: string, sectionId: string, field: string, fileId: string, queryRunner: QueryRunner): Promise<void>;
63
+ calculateProfileCompletion?(userId: string, user: ILoggedUserInfo): Promise<number>;
64
+ }
@@ -3,6 +3,7 @@ export interface IUserMini {
3
3
  id: string;
4
4
  email: string;
5
5
  name?: string;
6
+ phone?: string | null;
6
7
  profilePictureId?: string | null;
7
8
  }
8
9
  export interface IUser extends Identity {
@@ -5,6 +5,5 @@ export declare class AuthModule {
5
5
  static forRootAsync(asyncOptions: AuthModuleAsyncOptions): DynamicModule;
6
6
  private static buildDynamicModule;
7
7
  private static createAsyncProviders;
8
- private static getRepositoryProviders;
9
8
  static getEntities(enableCompanyFeature?: boolean): any[];
10
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-auth",
3
- "version": "1.0.0-beta",
3
+ "version": "1.0.0-rc",
4
4
  "description": "Authentication and authorization infrastructure for NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -84,14 +84,14 @@
84
84
  "bcrypt": "^5.0.0 || ^6.0.0",
85
85
  "class-transformer": "^0.5.0",
86
86
  "class-validator": "^0.14.0",
87
- "nodemailer": "^6.0.0",
87
+ "nodemailer": "^7.0.0",
88
88
  "passport": "^0.7.0",
89
89
  "passport-jwt": "^4.0.0",
90
90
  "passport-local": "^1.0.0",
91
91
  "typeorm": "^0.3.0"
92
92
  },
93
93
  "dependencies": {
94
- "@flusys/nestjs-core": "1.0.0-beta",
95
- "@flusys/nestjs-shared": "1.0.0-beta"
94
+ "@flusys/nestjs-core": "1.0.0-rc",
95
+ "@flusys/nestjs-shared": "1.0.0-rc"
96
96
  }
97
97
  }
@@ -1,8 +1,11 @@
1
1
  import { DatabaseMode, IDatabaseConfig, ITenantDatabaseConfig } from '@flusys/nestjs-core';
2
+ import { IModuleConfigService } from '@flusys/nestjs-shared/interfaces';
2
3
  import { AuthModuleOptions, IAuthModuleConfig } from '../interfaces/auth-module-options.interface';
3
- export declare class AuthConfigService {
4
+ export declare class AuthConfigService implements IModuleConfigService {
5
+ private readonly logger;
4
6
  private readonly options;
5
7
  private readonly config;
8
+ private refreshTokenSecretWarned;
6
9
  constructor(injectedOptions?: AuthModuleOptions);
7
10
  getDatabaseMode(): DatabaseMode;
8
11
  isMultiTenant(): boolean;
@@ -12,6 +15,7 @@ export declare class AuthConfigService {
12
15
  getRefreshTokenSecret(): string;
13
16
  getRefreshTokenExpiration(): string;
14
17
  getRefreshTokenCookieName(): string;
18
+ getFrontendUrl(): string | undefined;
15
19
  getDefaultDatabaseConfig(): IDatabaseConfig | undefined;
16
20
  getTenantDefaultDatabaseConfig(): IDatabaseConfig | undefined;
17
21
  getTenants(): ITenantDatabaseConfig[];
@@ -0,0 +1,51 @@
1
+ import { AuthDataSourceProvider } from './auth-datasource.provider';
2
+ import { PendingRegistration } from '../entities/pending-registration.entity';
3
+ import { IAuthEmailProvider } from '../interfaces';
4
+ export declare class AuthEmailService {
5
+ private readonly dataSourceProvider?;
6
+ private readonly emailProvider?;
7
+ private readonly logger;
8
+ private readonly PASSWORD_RESET_EXPIRY;
9
+ private readonly EMAIL_VERIFICATION_EXPIRY;
10
+ constructor(dataSourceProvider?: AuthDataSourceProvider, emailProvider?: IAuthEmailProvider);
11
+ private getUserRepository;
12
+ private getUserTokenRepository;
13
+ private getPendingRegistrationRepository;
14
+ isEmailEnabled(): boolean;
15
+ private generateToken;
16
+ private hashToken;
17
+ createPendingRegistration(data: {
18
+ email: string;
19
+ name?: string;
20
+ phone?: string;
21
+ password: string;
22
+ companyData?: PendingRegistration['companyData'];
23
+ verifyUrlBase: string;
24
+ }): Promise<{
25
+ success: boolean;
26
+ message: string;
27
+ }>;
28
+ forgotPassword(email: string, resetUrlBase: string): Promise<{
29
+ success: boolean;
30
+ message: string;
31
+ }>;
32
+ resetPassword(token: string, newPassword: string): Promise<{
33
+ success: boolean;
34
+ message: string;
35
+ }>;
36
+ verifyEmail(token: string): Promise<{
37
+ type: 'pending';
38
+ pending: PendingRegistration;
39
+ } | {
40
+ type: 'verified';
41
+ message: string;
42
+ }>;
43
+ resendVerificationEmail(email: string, verifyUrlBase: string): Promise<{
44
+ success: boolean;
45
+ message: string;
46
+ }>;
47
+ cleanupExpired(): Promise<{
48
+ deletedTokens: number;
49
+ deletedPending: number;
50
+ }>;
51
+ }