@flusys/nestjs-auth 1.0.0-beta → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +590 -473
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +365 -24
  4. package/cjs/controllers/branch.controller.js +44 -1
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +44 -1
  7. package/cjs/controllers/user-permission.controller.js +82 -154
  8. package/cjs/controllers/user.controller.js +211 -7
  9. package/cjs/docs/auth-swagger.config.js +14 -7
  10. package/cjs/dtos/authentication.dto.js +114 -38
  11. package/cjs/dtos/index.js +0 -1
  12. package/cjs/dtos/user-permission.dto.js +28 -0
  13. package/cjs/dtos/user.dto.js +19 -4
  14. package/cjs/entities/index.js +5 -1
  15. package/cjs/entities/pending-registration.entity.js +125 -0
  16. package/cjs/entities/user-company-permission.entity.js +3 -9
  17. package/cjs/entities/user-token.entity.js +129 -0
  18. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  19. package/cjs/interceptors/set-token.interceptor.js +15 -55
  20. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  21. package/cjs/interfaces/index.js +2 -0
  22. package/cjs/interfaces/user-enricher.interface.js +4 -0
  23. package/cjs/modules/auth.module.js +65 -143
  24. package/cjs/services/auth-config.service.js +25 -27
  25. package/cjs/services/auth-datasource.provider.js +13 -23
  26. package/cjs/services/auth-email.service.js +456 -0
  27. package/cjs/services/authentication.service.js +297 -349
  28. package/cjs/services/branch.service.js +9 -4
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/services/user-permission.service.js +43 -73
  32. package/cjs/services/user.service.js +184 -34
  33. package/cjs/strategies/jwt.strategy.js +44 -44
  34. package/cjs/validators/password.validator.js +45 -0
  35. package/config/auth.constants.d.ts +3 -0
  36. package/controllers/authentication.controller.d.ts +16 -10
  37. package/controllers/company-selection.controller.d.ts +6 -2
  38. package/controllers/user-permission.controller.d.ts +5 -4
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +20 -6
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/index.d.ts +0 -1
  44. package/dtos/user-permission.dto.d.ts +4 -0
  45. package/dtos/user.dto.d.ts +1 -2
  46. package/entities/index.d.ts +7 -3
  47. package/entities/pending-registration.entity.d.ts +17 -0
  48. package/entities/user-company-permission.entity.d.ts +1 -3
  49. package/entities/user-token.entity.d.ts +15 -0
  50. package/fesm/config/auth.constants.js +6 -2
  51. package/fesm/controllers/authentication.controller.js +368 -27
  52. package/fesm/controllers/branch.controller.js +45 -2
  53. package/fesm/controllers/company-selection.controller.js +28 -21
  54. package/fesm/controllers/company.controller.js +45 -2
  55. package/fesm/controllers/user-permission.controller.js +84 -156
  56. package/fesm/controllers/user.controller.js +214 -10
  57. package/fesm/docs/auth-swagger.config.js +14 -7
  58. package/fesm/dtos/authentication.dto.js +101 -39
  59. package/fesm/dtos/index.js +0 -1
  60. package/fesm/dtos/user-permission.dto.js +28 -0
  61. package/fesm/dtos/user.dto.js +20 -5
  62. package/fesm/entities/index.js +7 -1
  63. package/fesm/entities/pending-registration.entity.js +115 -0
  64. package/fesm/entities/user-company-permission.entity.js +3 -9
  65. package/fesm/entities/user-token.entity.js +113 -0
  66. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  67. package/fesm/interceptors/set-token.interceptor.js +14 -54
  68. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  69. package/fesm/interfaces/authentication.interface.js +1 -1
  70. package/fesm/interfaces/index.js +2 -0
  71. package/fesm/interfaces/user-enricher.interface.js +26 -0
  72. package/fesm/modules/auth.module.js +67 -145
  73. package/fesm/services/auth-config.service.js +26 -28
  74. package/fesm/services/auth-datasource.provider.js +14 -24
  75. package/fesm/services/auth-email.service.js +405 -0
  76. package/fesm/services/authentication.service.js +297 -349
  77. package/fesm/services/branch.service.js +9 -4
  78. package/fesm/services/company-selection-session.service.js +1 -1
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/services/user-permission.service.js +44 -74
  81. package/fesm/services/user.service.js +186 -36
  82. package/fesm/strategies/jwt.strategy.js +44 -44
  83. package/fesm/validators/password.validator.js +48 -0
  84. package/interceptors/clear-token.interceptor.d.ts +0 -1
  85. package/interceptors/set-token.interceptor.d.ts +1 -1
  86. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  87. package/interfaces/auth-module-options.interface.d.ts +4 -1
  88. package/interfaces/authentication.interface.d.ts +13 -3
  89. package/interfaces/index.d.ts +2 -0
  90. package/interfaces/user-enricher.interface.d.ts +64 -0
  91. package/interfaces/user.interface.d.ts +1 -0
  92. package/modules/auth.module.d.ts +6 -3
  93. package/package.json +4 -4
  94. package/services/auth-config.service.d.ts +7 -8
  95. package/services/auth-datasource.provider.d.ts +2 -4
  96. package/services/auth-email.service.d.ts +54 -0
  97. package/services/authentication.service.d.ts +27 -16
  98. package/services/company-selection-session.service.d.ts +1 -1
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +6 -9
  101. package/services/user.service.d.ts +19 -3
  102. package/strategies/jwt.strategy.d.ts +4 -1
  103. package/validators/password.validator.d.ts +10 -0
  104. package/cjs/dtos/user-company-permission.dto.js +0 -110
  105. package/dtos/user-company-permission.dto.d.ts +0 -10
  106. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -27,6 +27,7 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
+ import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
30
31
  import { Inject, Injectable, Scope } from '@nestjs/common';
31
32
  import { CompanyBranch } from '../entities/company-branch.entity';
32
33
  import { AuthConfigService } from './auth-config.service';
@@ -66,10 +67,14 @@ export class BranchService extends RequestScopedApiService {
66
67
  }
67
68
  async getExtraManipulateQuery(query, filterDto, user) {
68
69
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
69
- if (this.authConfig.isCompanyFeatureEnabled() && user?.companyId) {
70
- query.andWhere('company_branch.company_id = :companyId', {
71
- companyId: user.companyId
72
- });
70
+ // Only apply company filter if companyId is NOT explicitly provided in filter
71
+ // This prevents dual filtering when user selects a specific company
72
+ if (!filterDto?.filter?.companyId) {
73
+ applyCompanyFilter(query, {
74
+ isCompanyFeatureEnabled: this.authConfig.isCompanyFeatureEnabled(),
75
+ entityAlias: 'company_branch',
76
+ columnName: 'company_id'
77
+ }, user);
73
78
  }
74
79
  return result;
75
80
  }
@@ -33,7 +33,7 @@ const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
33
33
  export class CompanySelectionSessionService {
34
34
  /**
35
35
  * Create a new session for company/branch selection
36
- */ async createSession(userId, companyId) {
36
+ */ async createSession(userId) {
37
37
  const sessionId = randomUUID();
38
38
  const now = new Date();
39
39
  const expiresAt = new Date(now.getTime() + SESSION_TTL_MS);
@@ -1,4 +1,5 @@
1
1
  export * from './authentication.service';
2
+ export * from './auth-email.service';
2
3
  export * from './company-selection-session.service';
3
4
  export * from './user.service';
4
5
  export * from './company.service';
@@ -25,126 +25,96 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
28
+ import { ConflictException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
29
29
  import { PermissionType, UserCompanyPermission } from '../entities/user-company-permission.entity';
30
30
  import { AuthDataSourceProvider } from './auth-datasource.provider';
31
31
  export class UserPermissionService {
32
- /** Get repository lazily - initialized on first access */ async getRepository() {
32
+ async getRepository() {
33
33
  if (!this._repository) {
34
34
  this._repository = await this.dataSourceProvider.getRepository(UserCompanyPermission);
35
35
  }
36
36
  return this._repository;
37
37
  }
38
- // Company Permissions
39
- /** Find all companies a user has access to */ async findUserCompanies(userId) {
38
+ // Generic Permission Operations
39
+ /** Find all permissions of a specific type for a user */ async findUserPermissions(userId, type) {
40
40
  const repository = await this.getRepository();
41
41
  return repository.find({
42
42
  where: {
43
43
  userId,
44
- permissionType: PermissionType.COMPANY
44
+ permissionType: type
45
45
  },
46
46
  relations: [
47
- 'company'
47
+ type === PermissionType.COMPANY ? 'company' : 'branch'
48
48
  ],
49
49
  order: {
50
50
  createdAt: 'DESC'
51
51
  }
52
52
  });
53
53
  }
54
- /** Assign user to a company */ async assignUserToCompany(dto) {
54
+ /** Assign permission to user */ async assignPermission(userId, targetId, type, metadata) {
55
55
  const repository = await this.getRepository();
56
- // Check if already exists
56
+ const typeName = type === PermissionType.COMPANY ? 'company' : 'branch';
57
57
  const existing = await repository.findOne({
58
58
  where: {
59
- userId: dto.userId,
60
- targetId: dto.companyId,
61
- permissionType: PermissionType.COMPANY
59
+ userId,
60
+ targetId,
61
+ permissionType: type
62
62
  }
63
63
  });
64
64
  if (existing) {
65
- throw new BadRequestException('User already assigned to this company');
65
+ throw new ConflictException(`User already assigned to this ${typeName}`);
66
66
  }
67
- const permission = repository.create({
68
- userId: dto.userId,
69
- permissionType: PermissionType.COMPANY,
70
- targetId: dto.companyId,
71
- metadata: dto.metadata
72
- });
73
- await repository.save(permission);
74
- }
75
- /** Revoke user access to a company */ async revokeCompanyAccess(userId, companyId) {
76
- const repository = await this.getRepository();
77
- const permission = await repository.findOne({
78
- where: {
67
+ try {
68
+ await repository.save(repository.create({
79
69
  userId,
80
- targetId: companyId,
81
- permissionType: PermissionType.COMPANY
70
+ permissionType: type,
71
+ targetId,
72
+ metadata
73
+ }));
74
+ } catch (error) {
75
+ if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
76
+ throw new ConflictException(`User already assigned to this ${typeName}. Please refresh and try again.`);
82
77
  }
83
- });
84
- if (!permission) {
85
- throw new NotFoundException('Company permission not found');
78
+ throw error;
86
79
  }
87
- await repository.remove(permission);
88
80
  }
89
- // Branch Permissions
90
- /** Find all branches a user has access to */ async findUserBranches(userId) {
91
- const repository = await this.getRepository();
92
- return repository.find({
93
- where: {
94
- userId,
95
- permissionType: PermissionType.BRANCH
96
- },
97
- relations: [
98
- 'branch'
99
- ],
100
- order: {
101
- createdAt: 'DESC'
102
- }
103
- });
104
- }
105
- /** Assign user to a branch */ async assignUserToBranch(dto) {
106
- const repository = await this.getRepository();
107
- // Check if already exists
108
- const existing = await repository.findOne({
109
- where: {
110
- userId: dto.userId,
111
- targetId: dto.branchId,
112
- permissionType: PermissionType.BRANCH
113
- }
114
- });
115
- if (existing) {
116
- throw new BadRequestException('User already assigned to this branch');
117
- }
118
- const permission = repository.create({
119
- userId: dto.userId,
120
- permissionType: PermissionType.BRANCH,
121
- targetId: dto.branchId,
122
- metadata: dto.metadata
123
- });
124
- await repository.save(permission);
125
- }
126
- /** Revoke user access to a branch */ async revokeBranchAccess(userId, branchId) {
81
+ /** Revoke permission from user */ async revokePermission(userId, targetId, type) {
127
82
  const repository = await this.getRepository();
83
+ const typeName = type === PermissionType.COMPANY ? 'company' : 'branch';
128
84
  const permission = await repository.findOne({
129
85
  where: {
130
86
  userId,
131
- targetId: branchId,
132
- permissionType: PermissionType.BRANCH
87
+ targetId,
88
+ permissionType: type
133
89
  }
134
90
  });
135
91
  if (!permission) {
136
- throw new NotFoundException('Branch permission not found');
92
+ throw new NotFoundException(`${typeName.charAt(0).toUpperCase() + typeName.slice(1)} permission not found`);
137
93
  }
138
94
  await repository.remove(permission);
139
95
  }
140
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
96
+ /** Find user branch permissions with optional company filter */ async findUserBranches(userId, companyId) {
97
+ const repository = await this.getRepository();
98
+ const queryBuilder = repository.createQueryBuilder('permission').leftJoinAndSelect('permission.branch', 'branch').where('permission.userId = :userId', {
99
+ userId
100
+ }).andWhere('permission.permissionType = :type', {
101
+ type: PermissionType.BRANCH
102
+ }).orderBy('permission.createdAt', 'DESC');
103
+ if (companyId) {
104
+ queryBuilder.andWhere('branch.companyId = :companyId', {
105
+ companyId
106
+ });
107
+ }
108
+ return queryBuilder.getMany();
109
+ }
141
110
  constructor(dataSourceProvider){
142
111
  _define_property(this, "dataSourceProvider", void 0);
143
- _define_property(this, "logger", void 0);
144
112
  _define_property(this, "_repository", void 0);
113
+ // Convenience Methods
114
+ _define_property(this, "findUserCompanies", void 0);
145
115
  this.dataSourceProvider = dataSourceProvider;
146
- this.logger = new Logger(UserPermissionService.name);
147
116
  this._repository = null;
117
+ this.findUserCompanies = (userId)=>this.findUserPermissions(userId, PermissionType.COMPANY);
148
118
  }
149
119
  }
150
120
  UserPermissionService = _ts_decorate([
@@ -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,24 @@ export class UserService extends RequestScopedApiService {
58
59
  }
59
60
  }
60
61
  // Custom Methods
62
+ /**
63
+ * Validate profile ownership, ensure repository initialized, and fetch user.
64
+ * Throws ForbiddenException if not owner, NotFoundException if user not found.
65
+ */ async getUserForProfile(userId, user) {
66
+ if (userId !== user.id) {
67
+ throw new ForbiddenException('Cannot access another user\'s profile');
68
+ }
69
+ await this.ensureRepositoryInitialized();
70
+ const userEntity = await this.repository.findOne({
71
+ where: {
72
+ id: userId
73
+ }
74
+ });
75
+ if (!userEntity) {
76
+ throw new NotFoundException('User not found');
77
+ }
78
+ return userEntity;
79
+ }
61
80
  /** Verify email */ async verifyEmail(id) {
62
81
  // Ensure repository is initialized for custom method
63
82
  await this.ensureRepositoryInitialized();
@@ -119,33 +138,58 @@ export class UserService extends RequestScopedApiService {
119
138
  /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
120
139
  // Repository initialization is handled automatically by RequestScopedApiService
121
140
  const entity = await super.convertSingleDtoToEntity(dto, user);
122
- // Hash password if provided
123
- if (dto.password) {
141
+ // Hash password if provided (CreateUserDto has password field)
142
+ if ('password' in dto && dto.password) {
124
143
  entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
125
144
  }
126
145
  return entity;
127
146
  }
128
147
  /** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
148
+ if (isRaw) {
149
+ return this.convertRawToResponseDto(entity);
150
+ }
151
+ return this.convertEntityToDto(entity);
152
+ }
153
+ /** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
129
154
  // 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;
155
+ const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
132
156
  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,
157
+ id: raw.user_id,
158
+ name: raw.user_name,
159
+ phone: raw.user_phone,
160
+ email: raw.user_email,
161
+ profilePictureId: raw.user_profile_picture_id,
138
162
  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
163
+ emailVerified: raw.user_email_verified,
164
+ phoneVerified: raw.user_phone_verified,
165
+ lastLoginAt: raw.user_last_login_at,
166
+ additionalFields: raw.user_additional_fields,
167
+ createdAt: raw.user_created_at,
168
+ updatedAt: raw.user_updated_at,
169
+ deletedAt: raw.user_deleted_at,
170
+ createdById: raw.user_created_by_id,
171
+ updatedById: raw.user_updated_by_id,
172
+ deletedById: raw.user_deleted_by_id
173
+ };
174
+ }
175
+ /** Convert entity to response DTO */ convertEntityToDto(entity) {
176
+ return {
177
+ id: entity.id,
178
+ name: entity.name,
179
+ phone: entity.phone,
180
+ email: entity.email,
181
+ profilePictureId: entity.profilePictureId,
182
+ isActive: entity.isActive,
183
+ emailVerified: entity.emailVerified,
184
+ phoneVerified: entity.phoneVerified,
185
+ lastLoginAt: entity.lastLoginAt,
186
+ additionalFields: entity.additionalFields,
187
+ createdAt: entity.createdAt,
188
+ updatedAt: entity.updatedAt,
189
+ deletedAt: entity.deletedAt,
190
+ createdById: entity.createdById,
191
+ updatedById: entity.updatedById,
192
+ deletedById: entity.deletedById
149
193
  };
150
194
  }
151
195
  /** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
@@ -162,7 +206,11 @@ export class UserService extends RequestScopedApiService {
162
206
  }
163
207
  /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
164
208
  // Repository initialization is handled automatically by RequestScopedApiService
165
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
209
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
210
+ // Apply user enricher query modifications if available
211
+ if (this.userEnricher?.enrichListQuery) {
212
+ await this.userEnricher.enrichListQuery(query, user);
213
+ }
166
214
  // Only apply company filtering if:
167
215
  // 1. Company feature is enabled at bootstrap
168
216
  // 2. User has company context
@@ -282,16 +330,105 @@ export class UserService extends RequestScopedApiService {
282
330
  await queryRunner.release();
283
331
  }
284
332
  }
285
- /** Update user profile with password handling */ async profile(dto, user) {
286
- // Repository initialization is handled automatically by RequestScopedApiService
333
+ /**
334
+ * Get user profile with extras from enricher
335
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
336
+ */ async getProfileWithExtras(userId, user) {
337
+ const userEntity = await this.getUserForProfile(userId, user);
338
+ const userData = this.convertEntityToResponseDto(userEntity, false);
339
+ // Get extras from enricher if available
340
+ let extras;
341
+ if (this.userEnricher?.getProfileExtras) {
342
+ extras = await this.userEnricher.getProfileExtras(userId, user);
343
+ }
344
+ return {
345
+ user: userData,
346
+ extras
347
+ };
348
+ }
349
+ /**
350
+ * Get enriched user list
351
+ * Applies enricher transformations after query execution
352
+ */ async getEnrichedList(users, user) {
353
+ if (this.userEnricher?.enrichListItems) {
354
+ return this.userEnricher.enrichListItems(users, user);
355
+ }
356
+ return users;
357
+ }
358
+ // ============================================
359
+ // Multi-Step Profile Methods (via Enricher)
360
+ // ============================================
361
+ /**
362
+ * Get profile section definitions from enricher
363
+ */ async getProfileSections() {
364
+ if (this.userEnricher?.getProfileSections) {
365
+ return this.userEnricher.getProfileSections();
366
+ }
367
+ return [];
368
+ }
369
+ /**
370
+ * Get specific profile section data
371
+ */ async getProfileSectionData(userId, sectionId, user) {
372
+ await this.getUserForProfile(userId, user);
373
+ if (this.userEnricher?.getProfileSectionData) {
374
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
375
+ }
376
+ return null;
377
+ }
378
+ /**
379
+ * Update specific profile section
380
+ */ async updateProfileSection(userId, sectionId, data, user) {
381
+ const userEntity = await this.getUserForProfile(userId, user);
287
382
  if (!this.dataSource) {
288
383
  throw new BadRequestException('DataSource not available');
289
384
  }
385
+ if (!this.userEnricher?.updateProfileSection) {
386
+ throw new BadRequestException('Profile sections not supported');
387
+ }
388
+ const queryRunner = this.dataSource.createQueryRunner();
389
+ await queryRunner.connect();
390
+ await queryRunner.startTransaction();
391
+ try {
392
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
393
+ await queryRunner.commitTransaction();
394
+ await this.clearCacheForId([
395
+ userEntity
396
+ ]);
397
+ } catch (error) {
398
+ await queryRunner.rollbackTransaction();
399
+ ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
400
+ entity: this.entityName,
401
+ sectionId
402
+ });
403
+ ErrorHandler.rethrowError(error);
404
+ } finally{
405
+ await queryRunner.release();
406
+ }
407
+ }
408
+ /**
409
+ * Get profile completion percentage
410
+ */ async getProfileCompletion(userId, user) {
411
+ await this.getUserForProfile(userId, user);
412
+ if (this.userEnricher?.calculateProfileCompletion) {
413
+ return this.userEnricher.calculateProfileCompletion(userId, user);
414
+ }
415
+ return 100; // Default to 100% if no enricher
416
+ }
417
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
418
+ // Ensure repository and dataSource are initialized
419
+ await this.ensureRepositoryInitialized();
420
+ if (!this.dataSource) {
421
+ throw new BadRequestException('DataSource not available');
422
+ }
423
+ const userId = dto.id ?? user.id;
424
+ // Validate extras if enricher provides validation
425
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
426
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
427
+ }
290
428
  const queryRunner = this.dataSource.createQueryRunner();
291
429
  await queryRunner.connect();
292
430
  await queryRunner.startTransaction();
293
431
  try {
294
- const userId = dto.id ?? user.id;
295
432
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
296
433
  where: {
297
434
  id: userId
@@ -302,18 +439,28 @@ export class UserService extends RequestScopedApiService {
302
439
  }
303
440
  // Handle password update
304
441
  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
442
+ // If updating own profile, require old password verification
443
+ const isOwnProfile = !dto.id || dto.id === user.id;
444
+ if (isOwnProfile) {
445
+ if (!dto.oldPassword) {
446
+ throw new BadRequestException('Current password is required to change your password');
447
+ }
448
+ const isPasswordValid = await bcrypt.compare(dto.oldPassword, findUser.password);
449
+ if (!isPasswordValid) {
450
+ throw new BadRequestException('Current password is incorrect');
451
+ }
309
452
  }
310
- const salt = await bcrypt.genSalt();
311
- findUser.password = await bcrypt.hash(dto.password, salt);
453
+ findUser.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
312
454
  }
313
- // Merge other fields (excluding password)
314
- const { password, ...restDto } = dto;
455
+ // Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
456
+ // Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
457
+ const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
315
458
  Object.assign(findUser, restDto);
316
459
  await queryRunner.manager.save(this.repository.target, findUser);
460
+ // Handle extras update via enricher
461
+ if (extras && this.userEnricher?.updateProfileExtras) {
462
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
463
+ }
317
464
  await queryRunner.commitTransaction();
318
465
  await this.clearCacheForId([
319
466
  findUser
@@ -329,8 +476,8 @@ export class UserService extends RequestScopedApiService {
329
476
  await queryRunner.release();
330
477
  }
331
478
  }
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;
479
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
480
+ 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
481
  }
335
482
  }
336
483
  UserService = _ts_decorate([
@@ -341,11 +488,14 @@ UserService = _ts_decorate([
341
488
  _ts_param(1, Inject(UtilsService)),
342
489
  _ts_param(2, Inject(AuthDataSourceProvider)),
343
490
  _ts_param(3, Inject(AuthConfigService)),
491
+ _ts_param(4, Optional()),
492
+ _ts_param(4, Inject(USER_ENRICHER)),
344
493
  _ts_metadata("design:type", Function),
345
494
  _ts_metadata("design:paramtypes", [
346
495
  typeof HybridCache === "undefined" ? Object : HybridCache,
347
496
  typeof UtilsService === "undefined" ? Object : UtilsService,
348
497
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
349
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
498
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
499
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher
350
500
  ])
351
501
  ], UserService);
@@ -24,75 +24,75 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
24
24
  import { ModuleRef } from '@nestjs/core';
25
25
  import { PassportStrategy } from '@nestjs/passport';
26
26
  import { ExtractJwt, Strategy } from 'passport-jwt';
27
+ import { User } from '../entities/user.entity';
27
28
  import { AuthConfigService } from '../services/auth-config.service';
28
29
  import { AuthDataSourceProvider } from '../services/auth-datasource.provider';
29
30
  import { AuthenticationService } from '../services/authentication.service';
30
- import { User } from '../entities/user.entity';
31
- /**
32
- * Type guard to check if payload includes company context
33
- */ function isTokenPayloadWithCompany(payload) {
31
+ function hasCompanyContext(payload) {
34
32
  return 'companyId' in payload && 'branchId' in payload;
35
33
  }
36
34
  export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
37
35
  async validate(_req, payload) {
38
- // Resolve request-scoped services for this request
39
- let userRepository = null;
40
- let authService = null;
36
+ const userRepo = await this.resolveUserRepository();
37
+ await this.checkTokenRevocation(payload);
38
+ const user = await userRepo.findOne({
39
+ where: {
40
+ id: payload.id,
41
+ isActive: true
42
+ }
43
+ });
44
+ if (!user) {
45
+ throw new UnauthorizedException('User not found or inactive');
46
+ }
47
+ return this.buildUserInfo(payload);
48
+ }
49
+ async resolveUserRepository() {
41
50
  try {
42
- const dataSourceProvider = await this.moduleRef.resolve(AuthDataSourceProvider, undefined, {
51
+ const provider = await this.moduleRef.resolve(AuthDataSourceProvider, undefined, {
43
52
  strict: false
44
53
  });
45
- if (dataSourceProvider) {
46
- userRepository = await dataSourceProvider.getRepository(User);
54
+ if (provider) {
55
+ return provider.getRepository(User);
47
56
  }
48
- // Resolve AuthenticationService for token revocation check
49
- authService = await this.moduleRef.resolve(AuthenticationService, undefined, {
50
- strict: false
51
- });
52
- } catch (error) {
57
+ } catch {
53
58
  throw new UnauthorizedException('Unable to access user data');
54
59
  }
55
- if (!userRepository) {
56
- throw new UnauthorizedException('User repository not available');
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) {
60
+ throw new UnauthorizedException('User repository not available');
61
+ }
62
+ async checkTokenRevocation(payload) {
63
+ if (!payload.iat) return;
64
+ try {
65
+ const authService = await this.moduleRef.resolve(AuthenticationService, undefined, {
66
+ strict: false
67
+ });
68
+ if (authService && await authService.isTokenRevoked(payload.id, payload.iat)) {
62
69
  throw new UnauthorizedException('Token has been revoked');
63
70
  }
71
+ } catch (err) {
72
+ if (err instanceof UnauthorizedException) throw err;
64
73
  }
65
- // Find user and check if active
66
- const user = await userRepository.findOne({
67
- where: {
68
- id: payload.id,
69
- isActive: true
70
- }
71
- });
72
- if (!user) {
73
- throw new UnauthorizedException('User not found or inactive');
74
- }
75
- const result = {
74
+ }
75
+ buildUserInfo(payload) {
76
+ const base = {
76
77
  id: payload.id,
77
78
  email: payload.email
78
79
  };
79
- // Company context - use type guard for type safety
80
- if (isTokenPayloadWithCompany(payload)) {
81
- result.companyId = payload.companyId;
82
- // Only set branchId if it's not empty string
83
- if (payload.branchId) {
84
- result.branchId = payload.branchId;
85
- }
80
+ if (!hasCompanyContext(payload)) {
81
+ return base;
86
82
  }
87
- return result;
83
+ return {
84
+ ...base,
85
+ companyId: payload.companyId,
86
+ ...payload.branchId && {
87
+ branchId: payload.branchId
88
+ }
89
+ };
88
90
  }
89
91
  constructor(authConfig, moduleRef){
90
92
  super({
91
93
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
92
94
  ignoreExpiration: false,
93
- // Use secretOrKeyProvider instead of secretOrKey to defer secret lookup
94
- // This ensures AuthConfigService is fully initialized when using forRootAsync
95
- secretOrKeyProvider: (_request, _rawJwtToken, done)=>{
95
+ secretOrKeyProvider: (_req, _token, done)=>{
96
96
  done(null, this.authConfig.getJwtSecret());
97
97
  },
98
98
  passReqToCallback: true