@flusys/nestjs-auth 1.0.0-beta → 1.1.0-beta

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 (55) hide show
  1. package/cjs/config/auth.constants.js +4 -0
  2. package/cjs/controllers/authentication.controller.js +167 -5
  3. package/cjs/controllers/user.controller.js +151 -0
  4. package/cjs/dtos/authentication.dto.js +106 -3
  5. package/cjs/entities/index.js +5 -1
  6. package/cjs/entities/pending-registration.entity.js +125 -0
  7. package/cjs/entities/user-token.entity.js +129 -0
  8. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  9. package/cjs/interfaces/index.js +2 -0
  10. package/cjs/interfaces/user-enricher.interface.js +4 -0
  11. package/cjs/modules/auth.module.js +8 -2
  12. package/cjs/services/auth-config.service.js +4 -0
  13. package/cjs/services/auth-datasource.provider.js +4 -2
  14. package/cjs/services/auth-email.service.js +452 -0
  15. package/cjs/services/authentication.service.js +156 -37
  16. package/cjs/services/index.js +1 -0
  17. package/cjs/services/user.service.js +147 -11
  18. package/config/auth.constants.d.ts +1 -0
  19. package/controllers/authentication.controller.d.ts +13 -9
  20. package/controllers/user.controller.d.ts +27 -0
  21. package/dtos/authentication.dto.d.ts +21 -3
  22. package/dtos/user.dto.d.ts +1 -0
  23. package/entities/index.d.ts +7 -3
  24. package/entities/pending-registration.entity.d.ts +17 -0
  25. package/entities/user-token.entity.d.ts +15 -0
  26. package/fesm/config/auth.constants.js +11 -0
  27. package/fesm/controllers/authentication.controller.js +168 -6
  28. package/fesm/controllers/user.controller.js +151 -0
  29. package/fesm/dtos/authentication.dto.js +92 -3
  30. package/fesm/entities/index.js +7 -1
  31. package/fesm/entities/pending-registration.entity.js +115 -0
  32. package/fesm/entities/user-token.entity.js +113 -0
  33. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  34. package/fesm/interfaces/authentication.interface.js +1 -1
  35. package/fesm/interfaces/index.js +2 -0
  36. package/fesm/interfaces/user-enricher.interface.js +26 -0
  37. package/fesm/modules/auth.module.js +9 -3
  38. package/fesm/services/auth-config.service.js +4 -0
  39. package/fesm/services/auth-datasource.provider.js +4 -2
  40. package/fesm/services/auth-email.service.js +401 -0
  41. package/fesm/services/authentication.service.js +156 -37
  42. package/fesm/services/index.js +1 -0
  43. package/fesm/services/user.service.js +149 -13
  44. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  45. package/interfaces/auth-module-options.interface.d.ts +4 -1
  46. package/interfaces/authentication.interface.d.ts +14 -0
  47. package/interfaces/index.d.ts +2 -0
  48. package/interfaces/user-enricher.interface.d.ts +67 -0
  49. package/interfaces/user.interface.d.ts +1 -0
  50. package/package.json +3 -3
  51. package/services/auth-config.service.d.ts +1 -0
  52. package/services/auth-email.service.d.ts +50 -0
  53. package/services/authentication.service.d.ts +16 -4
  54. package/services/index.d.ts +1 -0
  55. package/services/user.service.d.ts +16 -3
@@ -36,6 +36,7 @@ import { AuthDataSourceProvider } from './auth-datasource.provider';
36
36
  import { PermissionType } from '../enums';
37
37
  import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
38
38
  import { HybridCache } from '@flusys/nestjs-shared/classes';
39
+ import { AuthEmailService } from './auth-email.service';
39
40
  import { CompanyBranch } from '../entities/company-branch.entity';
40
41
  import { Company } from '../entities/company.entity';
41
42
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
@@ -79,7 +80,14 @@ export class AuthenticationService {
79
80
  if (!user.isActive) {
80
81
  throw new UnauthorizedException('User account is deactivated');
81
82
  }
82
- // If company feature not enabled, return simple token
83
+ // Check email verification if email service is enabled
84
+ if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
85
+ return {
86
+ requiresEmailVerification: true,
87
+ email: user.email,
88
+ message: 'Please verify your email address before logging in.'
89
+ };
90
+ }
83
91
  if (!this.authConfig.isCompanyFeatureEnabled()) {
84
92
  return this.generateLoginResponse(user);
85
93
  }
@@ -136,10 +144,8 @@ export class AuthenticationService {
136
144
  return await this.generateCompanySelectionResponse(user, companies, branches);
137
145
  }
138
146
  // Registration
139
- /** Register a new user. If company feature enabled, optionally join/create company. */ async register(dto) {
147
+ /** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
140
148
  const userRepository = await this.getUserRepository();
141
- // Check if user exists
142
- // Note: Generic error message to prevent email enumeration attacks
143
149
  const existingUser = await userRepository.findOne({
144
150
  where: {
145
151
  email: dto.email
@@ -148,7 +154,33 @@ export class AuthenticationService {
148
154
  if (existingUser) {
149
155
  throw new BadRequestException('Registration failed. Please try again or contact support.');
150
156
  }
151
- // Create user
157
+ // If email verification is enabled, create pending registration instead of user
158
+ if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
159
+ const companyData = this.authConfig.isCompanyFeatureEnabled() ? {
160
+ companySlug: dto.companySlug,
161
+ branchSlug: dto.branchSlug,
162
+ newCompanyName: dto.newCompanyName,
163
+ newCompanyPhone: dto.newCompanyPhone,
164
+ newCompanyAddress: dto.newCompanyAddress
165
+ } : undefined;
166
+ const result = await this.authEmailService.createPendingRegistration({
167
+ email: dto.email,
168
+ name: dto.name,
169
+ phone: dto.phone,
170
+ password: dto.password,
171
+ companyData,
172
+ verifyUrlBase
173
+ });
174
+ return {
175
+ requiresEmailVerification: true,
176
+ message: result.message
177
+ };
178
+ }
179
+ // No email verification - create user directly
180
+ return this.createUserDirectly(dto);
181
+ }
182
+ /** Create user directly (no email verification) */ async createUserDirectly(dto) {
183
+ const userRepository = await this.getUserRepository();
152
184
  const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
153
185
  const user = userRepository.create({
154
186
  name: dto.name,
@@ -160,38 +192,68 @@ export class AuthenticationService {
160
192
  phoneVerified: false
161
193
  });
162
194
  await userRepository.save(user);
163
- // If company feature not enabled, return simple response
164
195
  if (!this.authConfig.isCompanyFeatureEnabled()) {
165
- return this.generateLoginResponse(user, false);
196
+ return this.generateRegistrationResponse(user, false, false);
197
+ }
198
+ return this.handleCompanyRegistration(user, dto, false);
199
+ }
200
+ /** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
201
+ const userRepository = await this.getUserRepository();
202
+ // Check if user already exists (race condition protection)
203
+ const existingUser = await userRepository.findOne({
204
+ where: {
205
+ email: pending.email
206
+ }
207
+ });
208
+ if (existingUser) {
209
+ throw new BadRequestException('This email has already been registered.');
210
+ }
211
+ // Create user with verified email
212
+ const user = userRepository.create({
213
+ name: pending.name,
214
+ email: pending.email,
215
+ phone: pending.phone,
216
+ password: pending.passwordHash,
217
+ isActive: true,
218
+ emailVerified: true,
219
+ phoneVerified: false
220
+ });
221
+ await userRepository.save(user);
222
+ // Handle company feature if enabled
223
+ if (this.authConfig.isCompanyFeatureEnabled() && pending.companyData) {
224
+ return this.handleCompanyRegistrationFromPending(user, pending.companyData);
166
225
  }
167
- // Handle company feature
168
- return this.handleCompanyRegistration(user, dto);
226
+ return this.generateRegistrationResponse(user, this.authConfig.isCompanyFeatureEnabled(), false);
227
+ }
228
+ /** Handle company registration from pending registration data */ async handleCompanyRegistrationFromPending(user, companyData) {
229
+ return this.processCompanyRegistration(user, companyData, false);
230
+ }
231
+ /** Handle company-related registration logic */ async handleCompanyRegistration(user, dto, requiresEmailVerification) {
232
+ return this.processCompanyRegistration(user, dto, requiresEmailVerification);
169
233
  }
170
- /** Handle company-related registration logic */ async handleCompanyRegistration(user, dto) {
234
+ /** Shared logic for company registration (join existing or create new) */ async processCompanyRegistration(user, data, requiresEmailVerification) {
171
235
  const companyRepository = await this.getCompanyRepository();
172
236
  const branchRepository = await this.getBranchRepository();
173
237
  const permissionRepository = await this.getPermissionRepository();
174
238
  if (!companyRepository || !branchRepository || !permissionRepository) {
175
- return this.generateLoginResponse(user, true);
239
+ return this.generateRegistrationResponse(user, true, requiresEmailVerification);
176
240
  }
177
241
  let company = null;
178
242
  let branch = null;
179
- // Case 1: Join existing company by slug
180
- if (dto.companySlug) {
243
+ if (data.companySlug) {
181
244
  company = await companyRepository.findOne({
182
245
  where: {
183
- slug: dto.companySlug,
246
+ slug: data.companySlug,
184
247
  isActive: true
185
248
  }
186
249
  });
187
250
  if (!company) {
188
251
  throw new BadRequestException('Company not found or inactive');
189
252
  }
190
- // Find branch by slug or get default
191
- if (dto.branchSlug) {
253
+ if (data.branchSlug) {
192
254
  branch = await branchRepository.findOne({
193
255
  where: {
194
- slug: dto.branchSlug,
256
+ slug: data.branchSlug,
195
257
  companyId: company.id,
196
258
  isActive: true
197
259
  }
@@ -200,7 +262,6 @@ export class AuthenticationService {
200
262
  throw new BadRequestException('Branch not found or inactive');
201
263
  }
202
264
  } else {
203
- // Get first active branch
204
265
  branch = await branchRepository.findOne({
205
266
  where: {
206
267
  companyId: company.id,
@@ -212,9 +273,8 @@ export class AuthenticationService {
212
273
  }
213
274
  });
214
275
  }
215
- } else if (dto.newCompanyName) {
216
- const slug = this.generateSlug(dto.newCompanyName);
217
- // Check if slug exists
276
+ } else if (data.newCompanyName) {
277
+ const slug = this.generateSlug(data.newCompanyName);
218
278
  const existingCompany = await companyRepository.findOne({
219
279
  where: {
220
280
  slug
@@ -223,16 +283,14 @@ export class AuthenticationService {
223
283
  if (existingCompany) {
224
284
  throw new BadRequestException('Company creation failed. Please try a different name.');
225
285
  }
226
- // Create company
227
286
  company = companyRepository.create({
228
- name: dto.newCompanyName,
287
+ name: data.newCompanyName,
229
288
  slug,
230
- phone: dto.newCompanyPhone,
231
- address: dto.newCompanyAddress,
289
+ phone: data.newCompanyPhone,
290
+ address: data.newCompanyAddress,
232
291
  isActive: true
233
292
  });
234
293
  await companyRepository.save(company);
235
- // Create default branch
236
294
  branch = branchRepository.create({
237
295
  name: 'Main Branch',
238
296
  slug: 'main-branch',
@@ -241,16 +299,13 @@ export class AuthenticationService {
241
299
  });
242
300
  await branchRepository.save(branch);
243
301
  }
244
- // Create permissions if company exists
245
302
  if (company) {
246
- // Create company permission
247
303
  const companyPermission = permissionRepository.create({
248
304
  userId: user.id,
249
305
  permissionType: PermissionType.COMPANY,
250
306
  targetId: company.id
251
307
  });
252
308
  await permissionRepository.save(companyPermission);
253
- // Create branch permission if branch exists
254
309
  if (branch) {
255
310
  const branchPermission = permissionRepository.create({
256
311
  userId: user.id,
@@ -259,10 +314,9 @@ export class AuthenticationService {
259
314
  });
260
315
  await permissionRepository.save(branchPermission);
261
316
  }
262
- return this.generateLoginResponseWithCompany(user, company, branch ?? undefined);
317
+ return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
263
318
  }
264
- // No company assigned
265
- return this.generateLoginResponse(user, true);
319
+ return this.generateRegistrationResponse(user, true, requiresEmailVerification);
266
320
  }
267
321
  /** Generate URL-friendly slug from name */ generateSlug(name) {
268
322
  return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
@@ -615,6 +669,7 @@ export class AuthenticationService {
615
669
  id: user.id,
616
670
  email: user.email,
617
671
  name: user.name ?? undefined,
672
+ phone: user.phone ?? undefined,
618
673
  profilePictureId: user.profilePictureId ?? undefined
619
674
  }
620
675
  };
@@ -701,6 +756,7 @@ export class AuthenticationService {
701
756
  id: user.id,
702
757
  email: user.email,
703
758
  name: user.name ?? undefined,
759
+ phone: user.phone ?? null,
704
760
  profilePictureId: user.profilePictureId ?? null
705
761
  },
706
762
  companyFeatureEnabled
@@ -736,6 +792,7 @@ export class AuthenticationService {
736
792
  id: user.id,
737
793
  email: user.email,
738
794
  name: user.name ?? undefined,
795
+ phone: user.phone ?? null,
739
796
  profilePictureId: user.profilePictureId ?? null
740
797
  },
741
798
  company: {
@@ -781,18 +838,76 @@ export class AuthenticationService {
781
838
  id: user.id,
782
839
  email: user.email,
783
840
  name: user.name ?? undefined,
841
+ phone: user.phone ?? null,
784
842
  profilePictureId: user.profilePictureId ?? null
785
843
  },
786
844
  companies: companiesWithBranches
787
845
  };
788
846
  }
847
+ /** Generate registration response - tokens included only if email verification not required */ generateRegistrationResponse(user, companyFeatureEnabled, requiresEmailVerification) {
848
+ const userInfo = {
849
+ id: user.id,
850
+ email: user.email,
851
+ name: user.name ?? undefined,
852
+ phone: user.phone ?? null,
853
+ profilePictureId: user.profilePictureId ?? null
854
+ };
855
+ if (requiresEmailVerification) {
856
+ return {
857
+ user: userInfo,
858
+ companyFeatureEnabled,
859
+ requiresEmailVerification: true
860
+ };
861
+ }
862
+ const loginResponse = this.generateLoginResponse(user, companyFeatureEnabled);
863
+ return {
864
+ ...loginResponse,
865
+ requiresEmailVerification: false
866
+ };
867
+ }
868
+ /** Generate registration response with company - tokens included only if email verification not required */ generateRegistrationResponseWithCompany(user, company, branch, requiresEmailVerification = false) {
869
+ const userInfo = {
870
+ id: user.id,
871
+ email: user.email,
872
+ name: user.name ?? undefined,
873
+ phone: user.phone ?? null,
874
+ profilePictureId: user.profilePictureId ?? null
875
+ };
876
+ const companyInfo = {
877
+ id: company.id,
878
+ name: company.name,
879
+ slug: company.slug,
880
+ logoId: company.logoId ?? null
881
+ };
882
+ const branchInfo = branch ? {
883
+ id: branch.id,
884
+ name: branch.name,
885
+ slug: branch.slug,
886
+ logoId: branch.logoId ?? null
887
+ } : undefined;
888
+ if (requiresEmailVerification) {
889
+ return {
890
+ user: userInfo,
891
+ company: companyInfo,
892
+ branch: branchInfo,
893
+ companyFeatureEnabled: true,
894
+ requiresEmailVerification: true
895
+ };
896
+ }
897
+ const loginResponse = this.generateLoginResponseWithCompany(user, company, branch);
898
+ return {
899
+ ...loginResponse,
900
+ requiresEmailVerification: false
901
+ };
902
+ }
789
903
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
790
- constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
904
+ constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
791
905
  _define_property(this, "jwtService", void 0);
792
906
  _define_property(this, "authConfig", void 0);
793
907
  _define_property(this, "cacheManager", void 0);
794
908
  _define_property(this, "dataSourceProvider", void 0);
795
909
  _define_property(this, "sessionService", void 0);
910
+ _define_property(this, "authEmailService", void 0);
796
911
  _define_property(this, "injectedUserRepository", void 0);
797
912
  _define_property(this, "injectedPermissionRepository", void 0);
798
913
  _define_property(this, "injectedCompanyRepository", void 0);
@@ -803,6 +918,7 @@ export class AuthenticationService {
803
918
  this.cacheManager = cacheManager;
804
919
  this.dataSourceProvider = dataSourceProvider;
805
920
  this.sessionService = sessionService;
921
+ this.authEmailService = authEmailService;
806
922
  this.injectedUserRepository = injectedUserRepository;
807
923
  this.injectedPermissionRepository = injectedPermissionRepository;
808
924
  this.injectedCompanyRepository = injectedCompanyRepository;
@@ -823,13 +939,15 @@ AuthenticationService = _ts_decorate([
823
939
  _ts_param(4, Optional()),
824
940
  _ts_param(4, Inject(CompanySelectionSessionService)),
825
941
  _ts_param(5, Optional()),
826
- _ts_param(5, InjectRepository(User)),
942
+ _ts_param(5, Inject(AuthEmailService)),
827
943
  _ts_param(6, Optional()),
828
- _ts_param(6, InjectRepository(UserCompanyPermission)),
944
+ _ts_param(6, InjectRepository(User)),
829
945
  _ts_param(7, Optional()),
830
- _ts_param(7, InjectRepository(Company)),
946
+ _ts_param(7, InjectRepository(UserCompanyPermission)),
831
947
  _ts_param(8, Optional()),
832
- _ts_param(8, InjectRepository(CompanyBranch)),
948
+ _ts_param(8, InjectRepository(Company)),
949
+ _ts_param(9, Optional()),
950
+ _ts_param(9, InjectRepository(CompanyBranch)),
833
951
  _ts_metadata("design:type", Function),
834
952
  _ts_metadata("design:paramtypes", [
835
953
  typeof JwtService === "undefined" ? Object : JwtService,
@@ -837,6 +955,7 @@ AuthenticationService = _ts_decorate([
837
955
  typeof HybridCache === "undefined" ? Object : HybridCache,
838
956
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
839
957
  typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
958
+ typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
840
959
  typeof Repository === "undefined" ? Object : Repository,
841
960
  typeof Repository === "undefined" ? Object : Repository,
842
961
  typeof Repository === "undefined" ? Object : Repository,
@@ -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';
@@ -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, 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 {
@@ -119,8 +120,8 @@ export class UserService extends RequestScopedApiService {
119
120
  /** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
120
121
  // Repository initialization is handled automatically by RequestScopedApiService
121
122
  const entity = await super.convertSingleDtoToEntity(dto, user);
122
- // Hash password if provided
123
- if (dto.password) {
123
+ // Hash password if provided (CreateUserDto has password field)
124
+ if ('password' in dto && dto.password) {
124
125
  entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
125
126
  }
126
127
  return entity;
@@ -162,7 +163,11 @@ export class UserService extends RequestScopedApiService {
162
163
  }
163
164
  /** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
164
165
  // Repository initialization is handled automatically by RequestScopedApiService
165
- const result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
166
+ let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
167
+ // Apply user enricher query modifications if available
168
+ if (this.userEnricher?.enrichListQuery) {
169
+ await this.userEnricher.enrichListQuery(query, user);
170
+ }
166
171
  // Only apply company filtering if:
167
172
  // 1. Company feature is enabled at bootstrap
168
173
  // 2. User has company context
@@ -282,16 +287,140 @@ export class UserService extends RequestScopedApiService {
282
287
  await queryRunner.release();
283
288
  }
284
289
  }
285
- /** Update user profile with password handling */ async profile(dto, user) {
286
- // Repository initialization is handled automatically by RequestScopedApiService
290
+ /**
291
+ * Get user profile with extras from enricher
292
+ * Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
293
+ */ async getProfileWithExtras(userId, user) {
294
+ await this.ensureRepositoryInitialized();
295
+ const userEntity = await this.repository.findOne({
296
+ where: {
297
+ id: userId
298
+ }
299
+ });
300
+ if (!userEntity) {
301
+ throw new NotFoundException('User not found');
302
+ }
303
+ const userData = this.convertEntityToResponseDto(userEntity, false);
304
+ // Get extras from enricher if available
305
+ let extras;
306
+ if (this.userEnricher?.getProfileExtras) {
307
+ extras = await this.userEnricher.getProfileExtras(userId, user);
308
+ }
309
+ return {
310
+ user: userData,
311
+ extras
312
+ };
313
+ }
314
+ /**
315
+ * Get enriched user list
316
+ * Applies enricher transformations after query execution
317
+ */ async getEnrichedList(users, user) {
318
+ if (this.userEnricher?.enrichListItems) {
319
+ return this.userEnricher.enrichListItems(users, user);
320
+ }
321
+ return users;
322
+ }
323
+ // ============================================
324
+ // Multi-Step Profile Methods (via Enricher)
325
+ // ============================================
326
+ /**
327
+ * Get profile section definitions from enricher
328
+ */ async getProfileSections() {
329
+ if (this.userEnricher?.getProfileSections) {
330
+ return this.userEnricher.getProfileSections();
331
+ }
332
+ return [];
333
+ }
334
+ /**
335
+ * Get specific profile section data
336
+ */ async getProfileSectionData(userId, sectionId, user) {
337
+ await this.ensureRepositoryInitialized();
338
+ // Verify user exists
339
+ const userEntity = await this.repository.findOne({
340
+ where: {
341
+ id: userId
342
+ }
343
+ });
344
+ if (!userEntity) {
345
+ throw new NotFoundException('User not found');
346
+ }
347
+ if (this.userEnricher?.getProfileSectionData) {
348
+ return this.userEnricher.getProfileSectionData(userId, sectionId, user);
349
+ }
350
+ return null;
351
+ }
352
+ /**
353
+ * Update specific profile section
354
+ */ async updateProfileSection(userId, sectionId, data, user) {
355
+ await this.ensureRepositoryInitialized();
356
+ if (!this.dataSource) {
357
+ throw new BadRequestException('DataSource not available');
358
+ }
359
+ // Verify user exists
360
+ const userEntity = await this.repository.findOne({
361
+ where: {
362
+ id: userId
363
+ }
364
+ });
365
+ if (!userEntity) {
366
+ throw new NotFoundException('User not found');
367
+ }
368
+ if (!this.userEnricher?.updateProfileSection) {
369
+ throw new BadRequestException('Profile sections not supported');
370
+ }
371
+ const queryRunner = this.dataSource.createQueryRunner();
372
+ await queryRunner.connect();
373
+ await queryRunner.startTransaction();
374
+ try {
375
+ await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
376
+ await queryRunner.commitTransaction();
377
+ await this.clearCacheForId([
378
+ userEntity
379
+ ]);
380
+ } catch (error) {
381
+ await queryRunner.rollbackTransaction();
382
+ ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
383
+ entity: this.entityName,
384
+ sectionId
385
+ });
386
+ ErrorHandler.rethrowError(error);
387
+ } finally{
388
+ await queryRunner.release();
389
+ }
390
+ }
391
+ /**
392
+ * Get profile completion percentage
393
+ */ async getProfileCompletion(userId, user) {
394
+ await this.ensureRepositoryInitialized();
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?.calculateProfileCompletion) {
405
+ return this.userEnricher.calculateProfileCompletion(userId, user);
406
+ }
407
+ return 100; // Default to 100% if no enricher
408
+ }
409
+ /** Update user profile with password handling and extras */ async profile(dto, user) {
410
+ // Ensure repository and dataSource are initialized
411
+ await this.ensureRepositoryInitialized();
287
412
  if (!this.dataSource) {
288
413
  throw new BadRequestException('DataSource not available');
289
414
  }
415
+ const userId = dto.id ?? user.id;
416
+ // Validate extras if enricher provides validation
417
+ if (dto.extras && this.userEnricher?.validateProfileExtras) {
418
+ await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
419
+ }
290
420
  const queryRunner = this.dataSource.createQueryRunner();
291
421
  await queryRunner.connect();
292
422
  await queryRunner.startTransaction();
293
423
  try {
294
- const userId = dto.id ?? user.id;
295
424
  const findUser = await queryRunner.manager.findOne(this.repository.target, {
296
425
  where: {
297
426
  id: userId
@@ -310,10 +439,14 @@ export class UserService extends RequestScopedApiService {
310
439
  const salt = await bcrypt.genSalt();
311
440
  findUser.password = await bcrypt.hash(dto.password, salt);
312
441
  }
313
- // Merge other fields (excluding password)
314
- const { password, ...restDto } = dto;
442
+ // Merge other fields (excluding password and extras)
443
+ const { password, extras, ...restDto } = dto;
315
444
  Object.assign(findUser, restDto);
316
445
  await queryRunner.manager.save(this.repository.target, findUser);
446
+ // Handle extras update via enricher
447
+ if (extras && this.userEnricher?.updateProfileExtras) {
448
+ await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
449
+ }
317
450
  await queryRunner.commitTransaction();
318
451
  await this.clearCacheForId([
319
452
  findUser
@@ -329,8 +462,8 @@ export class UserService extends RequestScopedApiService {
329
462
  await queryRunner.release();
330
463
  }
331
464
  }
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;
465
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
466
+ 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
467
  }
335
468
  }
336
469
  UserService = _ts_decorate([
@@ -341,11 +474,14 @@ UserService = _ts_decorate([
341
474
  _ts_param(1, Inject(UtilsService)),
342
475
  _ts_param(2, Inject(AuthDataSourceProvider)),
343
476
  _ts_param(3, Inject(AuthConfigService)),
477
+ _ts_param(4, Optional()),
478
+ _ts_param(4, Inject(USER_ENRICHER)),
344
479
  _ts_metadata("design:type", Function),
345
480
  _ts_metadata("design:paramtypes", [
346
481
  typeof HybridCache === "undefined" ? Object : HybridCache,
347
482
  typeof UtilsService === "undefined" ? Object : UtilsService,
348
483
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
349
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
484
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
485
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher
350
486
  ])
351
487
  ], UserService);
@@ -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';