@flusys/nestjs-auth 3.0.0-rc → 3.0.1

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 (40) hide show
  1. package/README.md +610 -95
  2. package/cjs/controllers/authentication.controller.js +2 -173
  3. package/cjs/controllers/email-verification.controller.js +220 -0
  4. package/cjs/controllers/index.js +1 -0
  5. package/cjs/dtos/authentication.dto.js +12 -2
  6. package/cjs/entities/index.js +16 -17
  7. package/cjs/entities/pending-registration.entity.js +9 -0
  8. package/cjs/modules/auth.module.js +33 -24
  9. package/cjs/services/auth-config.service.js +11 -7
  10. package/cjs/services/auth-datasource.provider.js +15 -27
  11. package/cjs/services/auth-email.service.js +8 -33
  12. package/cjs/services/authentication.service.js +155 -110
  13. package/cjs/services/user.service.js +28 -83
  14. package/controllers/authentication.controller.d.ts +2 -10
  15. package/controllers/email-verification.controller.d.ts +20 -0
  16. package/controllers/index.d.ts +1 -0
  17. package/dtos/authentication.dto.d.ts +1 -0
  18. package/entities/index.d.ts +5 -10
  19. package/entities/pending-registration.entity.d.ts +1 -0
  20. package/fesm/controllers/authentication.controller.js +3 -174
  21. package/fesm/controllers/email-verification.controller.js +210 -0
  22. package/fesm/controllers/index.js +1 -0
  23. package/fesm/dtos/authentication.dto.js +32 -25
  24. package/fesm/entities/index.js +17 -9
  25. package/fesm/entities/pending-registration.entity.js +9 -0
  26. package/fesm/interfaces/user-enricher.interface.js +2 -24
  27. package/fesm/modules/auth.module.js +35 -26
  28. package/fesm/services/auth-config.service.js +11 -7
  29. package/fesm/services/auth-datasource.provider.js +15 -27
  30. package/fesm/services/auth-email.service.js +8 -33
  31. package/fesm/services/authentication.service.js +145 -100
  32. package/fesm/services/user.service.js +28 -83
  33. package/interfaces/user-enricher.interface.d.ts +3 -2
  34. package/modules/auth.module.d.ts +3 -1
  35. package/package.json +3 -3
  36. package/services/auth-config.service.d.ts +1 -0
  37. package/services/auth-datasource.provider.d.ts +1 -0
  38. package/services/auth-email.service.d.ts +1 -0
  39. package/services/authentication.service.d.ts +7 -1
  40. package/services/user.service.d.ts +5 -0
@@ -26,11 +26,14 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { HybridCache } from '@flusys/nestjs-shared/classes';
29
+ import { INotificationAdapter, NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
30
+ import { NotificationType } from '@flusys/nestjs-shared/enums';
29
31
  import { ErrorHandler, generateUniqueSlug } from '@flusys/nestjs-shared/utils';
30
32
  import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
31
33
  import { JwtService } from '@nestjs/jwt';
32
34
  import * as bcrypt from 'bcrypt';
33
- import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
35
+ import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX, USER_ENRICHER } from '../config';
36
+ import { IUserEnricher } from '../interfaces/user-enricher.interface';
34
37
  import { CompanyBranch } from '../entities/company-branch.entity';
35
38
  import { Company } from '../entities/company.entity';
36
39
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
@@ -41,11 +44,14 @@ import { AuthDataSourceProvider } from './auth-datasource.provider';
41
44
  import { AuthEmailService } from './auth-email.service';
42
45
  import { CompanySelectionSessionService } from './company-selection-session.service';
43
46
  export class AuthenticationService {
44
- // Feature Flags
45
- /** Check if company feature is enabled (tenant-aware with global fallback) */ isCompanyFeatureEnabled() {
47
+ isCompanyFeatureEnabled() {
46
48
  return this.dataSourceProvider?.isCompanyFeatureEnabled() ?? this.authConfig.isCompanyFeatureEnabled();
47
49
  }
48
- // Repository Getters
50
+ isEmailVerificationEnabled() {
51
+ const featureEnabled = this.dataSourceProvider?.isEmailVerificationEnabled() ?? this.authConfig.isEmailVerificationEnabled();
52
+ const providerAvailable = this.authEmailService?.isEmailEnabled() ?? false;
53
+ return featureEnabled && providerAvailable;
54
+ }
49
55
  async getUserRepository() {
50
56
  if (!this.dataSourceProvider) {
51
57
  throw new Error('User repository not available');
@@ -64,10 +70,8 @@ export class AuthenticationService {
64
70
  if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
65
71
  return this.dataSourceProvider.getRepository(CompanyBranch);
66
72
  }
67
- // Login
68
73
  async login(email, password) {
69
- // Check for pending registration first (user registered but hasn't verified email yet)
70
- if (await this.authEmailService?.hasPendingRegistration(email)) {
74
+ if (this.isEmailVerificationEnabled() && await this.authEmailService.hasPendingRegistration(email)) {
71
75
  return {
72
76
  requiresEmailVerification: true,
73
77
  email,
@@ -78,7 +82,7 @@ export class AuthenticationService {
78
82
  if (!user.isActive) {
79
83
  throw new UnauthorizedException('User account is deactivated');
80
84
  }
81
- if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
85
+ if (this.isEmailVerificationEnabled() && !user.emailVerified) {
82
86
  return {
83
87
  requiresEmailVerification: true,
84
88
  email: user.email,
@@ -90,20 +94,18 @@ export class AuthenticationService {
90
94
  }
91
95
  return this.handleCompanyBasedLogin(user);
92
96
  }
93
- /** Handle login when company feature is enabled */ async handleCompanyBasedLogin(user) {
97
+ async handleCompanyBasedLogin(user) {
94
98
  const permissionRepository = await this.getPermissionRepository();
95
99
  const branchRepository = await this.getBranchRepository();
96
100
  if (!permissionRepository || !branchRepository) {
97
101
  return this.generateLoginResponse(user);
98
102
  }
99
- // Get company permissions
100
103
  const companyPermissions = await permissionRepository.find({
101
104
  where: {
102
105
  userId: user.id,
103
106
  permissionType: PermissionType.COMPANY
104
107
  }
105
108
  });
106
- // Load companies
107
109
  const companyRepository = await this.getCompanyRepository();
108
110
  if (!companyRepository) {
109
111
  return this.generateLoginResponse(user);
@@ -114,12 +116,9 @@ export class AuthenticationService {
114
116
  }).andWhere('c.is_active = :active', {
115
117
  active: true
116
118
  }).getMany() : [];
117
- // If user has no company assignments yet, return simple login response
118
- // This allows users to login and be assigned to companies later
119
119
  if (companies.length === 0) {
120
120
  return this.generateLoginResponse(user);
121
121
  }
122
- // Get branch permissions
123
122
  const branchPermissions = await permissionRepository.find({
124
123
  where: {
125
124
  userId: user.id,
@@ -132,32 +131,25 @@ export class AuthenticationService {
132
131
  }).andWhere('b.is_active = :active', {
133
132
  active: true
134
133
  }).getMany() : [];
135
- // Case 1: Single company, single branch → Direct login
136
134
  if (companies.length === 1 && branches.length === 1) {
137
135
  return this.generateLoginResponseWithCompany(user, companies[0], branches[0]);
138
136
  }
139
- // Case 2: Multiple options → Selection required (frontend handles UI)
140
- // Returns all companies with their branches for frontend to display
141
137
  return await this.generateCompanySelectionResponse(user, companies, branches);
142
138
  }
143
- // Registration
144
- /** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
139
+ async register(dto, verifyUrlBase) {
145
140
  const userRepository = await this.getUserRepository();
146
141
  const existingUser = await userRepository.findOne({
147
142
  where: {
148
143
  email: dto.email
149
144
  }
150
145
  });
151
- // When company feature is enabled and user exists, allow joining/creating company
152
146
  if (existingUser && this.isCompanyFeatureEnabled()) {
153
147
  return this.handleExistingUserCompanyRegistration(existingUser, dto);
154
148
  }
155
- // User exists but company feature is off - email must be unique
156
149
  if (existingUser) {
157
150
  throw new BadRequestException('Registration failed due to email exists. Please try again or contact support.');
158
151
  }
159
- // If email verification is enabled, create pending registration instead of user
160
- if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
152
+ if (this.isEmailVerificationEnabled() && verifyUrlBase) {
161
153
  const companyData = this.isCompanyFeatureEnabled() ? {
162
154
  companySlug: dto.companySlug,
163
155
  branchSlug: dto.branchSlug,
@@ -171,6 +163,7 @@ export class AuthenticationService {
171
163
  phone: dto.phone,
172
164
  password: dto.password,
173
165
  companyData,
166
+ additionalFields: dto.additionalFields,
174
167
  verifyUrlBase
175
168
  });
176
169
  return {
@@ -178,10 +171,9 @@ export class AuthenticationService {
178
171
  message: result.message
179
172
  };
180
173
  }
181
- // No email verification - create user directly
182
174
  return this.createUserDirectly(dto);
183
175
  }
184
- /** Handle registration for existing user when company feature is enabled */ async handleExistingUserCompanyRegistration(existingUser, dto) {
176
+ async handleExistingUserCompanyRegistration(existingUser, dto) {
185
177
  if (!dto.companySlug && !dto.newCompanyName) {
186
178
  throw new BadRequestException('Email already registered. Please login or provide a company to join/create.');
187
179
  }
@@ -190,7 +182,6 @@ export class AuthenticationService {
190
182
  if (!permissionRepository || !companyRepository) {
191
183
  throw new BadRequestException('Company feature repositories not available');
192
184
  }
193
- // For existing users joining a company, check if already assigned
194
185
  if (dto.companySlug) {
195
186
  const company = await companyRepository.findOne({
196
187
  where: {
@@ -213,50 +204,92 @@ export class AuthenticationService {
213
204
  }
214
205
  return this.processCompanyRegistration(existingUser, dto, true);
215
206
  }
216
- /** Create user directly (no email verification) */ async createUserDirectly(dto) {
217
- const userRepository = await this.getUserRepository();
218
- const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
219
- const user = userRepository.create({
220
- name: dto.name,
221
- email: dto.email,
222
- phone: dto.phone,
223
- password: hashedPassword,
224
- isActive: true,
225
- emailVerified: false,
226
- phoneVerified: false
227
- });
228
- await userRepository.save(user);
229
- if (!this.isCompanyFeatureEnabled()) {
230
- return this.generateRegistrationResponse(user);
207
+ async createUserDirectly(dto) {
208
+ if (!this.dataSourceProvider) {
209
+ throw new BadRequestException('Data source not available');
210
+ }
211
+ const dataSource = await this.dataSourceProvider.getDataSource();
212
+ const queryRunner = dataSource.createQueryRunner();
213
+ await queryRunner.connect();
214
+ await queryRunner.startTransaction();
215
+ try {
216
+ const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
217
+ const user = queryRunner.manager.create(User, {
218
+ name: dto.name,
219
+ email: dto.email,
220
+ phone: dto.phone,
221
+ password: hashedPassword,
222
+ isActive: true,
223
+ emailVerified: false,
224
+ phoneVerified: false
225
+ });
226
+ await queryRunner.manager.save(User, user);
227
+ if (this.userEnricher?.onUserCreated) {
228
+ await this.userEnricher.onUserCreated(user.id, dto.additionalFields ?? null, queryRunner);
229
+ }
230
+ await queryRunner.commitTransaction();
231
+ if (!this.isCompanyFeatureEnabled()) {
232
+ return this.generateRegistrationResponse(user);
233
+ }
234
+ return this.processCompanyRegistration(user, dto);
235
+ } catch (error) {
236
+ await queryRunner.rollbackTransaction();
237
+ if (error instanceof BadRequestException) {
238
+ throw error;
239
+ }
240
+ ErrorHandler.logError(this.logger, error, 'createUserDirectly');
241
+ throw new BadRequestException('Registration failed. Please try again.');
242
+ } finally{
243
+ await queryRunner.release();
231
244
  }
232
- return this.processCompanyRegistration(user, dto);
233
245
  }
234
- /** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
235
- const userRepository = await this.getUserRepository();
236
- const existingUser = await userRepository.findOne({
237
- where: {
238
- email: pending.email
246
+ async createUserFromPendingRegistration(pending) {
247
+ if (!this.dataSourceProvider) {
248
+ throw new BadRequestException('Data source not available');
249
+ }
250
+ const dataSource = await this.dataSourceProvider.getDataSource();
251
+ const queryRunner = dataSource.createQueryRunner();
252
+ await queryRunner.connect();
253
+ await queryRunner.startTransaction();
254
+ try {
255
+ const existingUser = await queryRunner.manager.findOne(User, {
256
+ where: {
257
+ email: pending.email
258
+ }
259
+ });
260
+ if (existingUser) {
261
+ throw new BadRequestException('This email has already been registered.');
239
262
  }
240
- });
241
- if (existingUser) {
242
- throw new BadRequestException('This email has already been registered.');
243
- }
244
- const user = userRepository.create({
245
- name: pending.name,
246
- email: pending.email,
247
- phone: pending.phone,
248
- password: pending.passwordHash,
249
- isActive: true,
250
- emailVerified: true,
251
- phoneVerified: false
252
- });
253
- await userRepository.save(user);
254
- if (this.isCompanyFeatureEnabled() && pending.companyData) {
255
- return this.processCompanyRegistration(user, pending.companyData);
263
+ const user = queryRunner.manager.create(User, {
264
+ name: pending.name,
265
+ email: pending.email,
266
+ phone: pending.phone,
267
+ password: pending.passwordHash,
268
+ isActive: true,
269
+ emailVerified: true,
270
+ phoneVerified: false
271
+ });
272
+ await queryRunner.manager.save(User, user);
273
+ if (this.userEnricher?.onUserCreated) {
274
+ await this.userEnricher.onUserCreated(user.id, pending.additionalFields ?? null, queryRunner);
275
+ }
276
+ await queryRunner.commitTransaction();
277
+ if (this.isCompanyFeatureEnabled() && pending.companyData) {
278
+ return this.processCompanyRegistration(user, pending.companyData);
279
+ }
280
+ return this.generateRegistrationResponse(user);
281
+ } catch (error) {
282
+ await queryRunner.rollbackTransaction();
283
+ if (error instanceof BadRequestException) {
284
+ throw error;
285
+ }
286
+ ErrorHandler.logError(this.logger, error, 'createUserFromPendingRegistration');
287
+ throw new BadRequestException('Registration failed. Please try again.');
288
+ } finally{
289
+ await queryRunner.release();
256
290
  }
257
- return this.generateRegistrationResponse(user);
258
291
  }
259
- /** Process company registration (join existing or create new) */ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
292
+ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
260
293
  const companyRepository = await this.getCompanyRepository();
261
294
  const branchRepository = await this.getBranchRepository();
262
295
  const permissionRepository = await this.getPermissionRepository();
@@ -271,7 +304,7 @@ export class AuthenticationService {
271
304
  }
272
305
  return this.generateRegistrationResponse(user);
273
306
  }
274
- /** Find existing company/branch or create new ones */ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
307
+ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
275
308
  if (data.companySlug) {
276
309
  const company = await companyRepo.findOne({
277
310
  where: {
@@ -335,7 +368,7 @@ export class AuthenticationService {
335
368
  branch: null
336
369
  };
337
370
  }
338
- /** Assign user to company and optionally branch */ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
371
+ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
339
372
  await permissionRepo.save(permissionRepo.create({
340
373
  userId,
341
374
  permissionType: PermissionType.COMPANY,
@@ -349,8 +382,7 @@ export class AuthenticationService {
349
382
  }));
350
383
  }
351
384
  }
352
- // Company/Branch Selection
353
- /** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
385
+ async select(sessionId, companyId, branchId) {
354
386
  this.ensureCompanyFeatureEnabled();
355
387
  if (!this.sessionService) {
356
388
  throw new BadRequestException('Session service not available');
@@ -361,11 +393,11 @@ export class AuthenticationService {
361
393
  }
362
394
  return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
363
395
  }
364
- /** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
396
+ async switchCompany(userId, companyId, branchId) {
365
397
  this.ensureCompanyFeatureEnabled();
366
398
  return this.validateAndLoginWithCompany(userId, companyId, branchId);
367
399
  }
368
- /** Shared: Validate user access to company/branch and generate login response */ async validateAndLoginWithCompany(userId, companyId, branchId) {
400
+ async validateAndLoginWithCompany(userId, companyId, branchId) {
369
401
  const userRepository = await this.getUserRepository();
370
402
  const user = await userRepository.findOne({
371
403
  where: {
@@ -381,7 +413,6 @@ export class AuthenticationService {
381
413
  if (!permissionRepository || !companyRepository || !branchRepository) {
382
414
  throw new BadRequestException('Repositories not available');
383
415
  }
384
- // Validate company access
385
416
  const hasCompanyAccess = await permissionRepository.findOne({
386
417
  where: {
387
418
  userId,
@@ -401,7 +432,6 @@ export class AuthenticationService {
401
432
  if (!company) {
402
433
  throw new UnauthorizedException('Company not found or inactive');
403
434
  }
404
- // Validate branch access
405
435
  const hasBranchAccess = await permissionRepository.findOne({
406
436
  where: {
407
437
  userId,
@@ -424,8 +454,7 @@ export class AuthenticationService {
424
454
  }
425
455
  return this.generateLoginResponseWithCompany(user, company, branch);
426
456
  }
427
- // Token Management
428
- /** Refresh access token (with token rotation). Returns new access token and new refresh token. */ async refreshToken(refreshToken) {
457
+ async refreshToken(refreshToken) {
429
458
  try {
430
459
  const payload = this.jwtService.verify(refreshToken, {
431
460
  secret: this.authConfig.getRefreshTokenSecret()
@@ -459,7 +488,7 @@ export class AuthenticationService {
459
488
  throw new UnauthorizedException('Invalid or expired refresh token');
460
489
  }
461
490
  }
462
- /** Change password */ async changePassword(userId, currentPassword, newPassword) {
491
+ async changePassword(userId, currentPassword, newPassword) {
463
492
  const userRepository = await this.getUserRepository();
464
493
  const user = await userRepository.findOne({
465
494
  where: {
@@ -473,21 +502,19 @@ export class AuthenticationService {
473
502
  user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
474
503
  await userRepository.save(user);
475
504
  }
476
- /** Logout - Revokes all tokens. Stores revocation timestamp in cache. */ async logout(userId) {
505
+ async logout(userId) {
477
506
  if (!this.cacheManager) {
478
- // Cache is required for secure logout - tokens cannot be revoked without it
479
507
  this.logger.error(`SECURITY: Cache not available for logout (userId: ${userId}). ` + 'Token revocation failed - tokens will remain valid until expiry. ' + 'Configure cache (Redis) to enable proper token revocation.');
480
508
  throw new BadRequestException('Logout service temporarily unavailable. Please try again later.');
481
509
  }
482
- const revokedAt = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
510
+ const revokedAt = Math.floor(Date.now() / 1000);
483
511
  const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
484
- // TTL matches refresh token expiry (tokens older than this are already expired)
485
512
  const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
486
513
  const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
487
514
  await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
488
515
  this.logger.log(`User ${userId} logged out - all tokens revoked`);
489
516
  }
490
- /** Check if a token is revoked (issued before user's revocation timestamp) */ async isTokenRevoked(userId, tokenIssuedAt) {
517
+ async isTokenRevoked(userId, tokenIssuedAt) {
491
518
  if (!this.cacheManager || !tokenIssuedAt) {
492
519
  return false;
493
520
  }
@@ -496,12 +523,11 @@ export class AuthenticationService {
496
523
  if (!revokedAt) {
497
524
  return false;
498
525
  }
499
- // Token is revoked if it was issued before the revocation timestamp
500
526
  return tokenIssuedAt < revokedAt;
501
527
  }
502
- /** Parse JWT expiry string to milliseconds */ parseExpiryToMs(expiry) {
528
+ parseExpiryToMs(expiry) {
503
529
  const match = expiry.match(/^(\d+)([smhd])$/);
504
- if (!match) return 7 * 24 * 60 * 60 * 1000; // Default 7 days
530
+ if (!match) return 7 * 24 * 60 * 60 * 1000;
505
531
  const value = parseInt(match[1], 10);
506
532
  const unit = match[2];
507
533
  switch(unit){
@@ -517,8 +543,7 @@ export class AuthenticationService {
517
543
  return 7 * 24 * 60 * 60 * 1000;
518
544
  }
519
545
  }
520
- // Query Methods
521
- /** Get user's companies */ async getUserCompanies(userId) {
546
+ async getUserCompanies(userId) {
522
547
  this.ensureCompanyFeatureEnabled();
523
548
  const permissionRepository = await this.getPermissionRepository();
524
549
  const companyRepository = await this.getCompanyRepository();
@@ -539,14 +564,13 @@ export class AuthenticationService {
539
564
  active: true
540
565
  }).orderBy('c.name', 'ASC').getMany();
541
566
  }
542
- /** Get branches for a company that user has access to */ async getUserBranches(userId, companyId) {
567
+ async getUserBranches(userId, companyId) {
543
568
  this.ensureCompanyFeatureEnabled();
544
569
  const permissionRepository = await this.getPermissionRepository();
545
570
  const branchRepository = await this.getBranchRepository();
546
571
  if (!permissionRepository || !branchRepository) {
547
572
  throw new BadRequestException('Repositories not available');
548
573
  }
549
- // Verify company access
550
574
  const companyPermission = await permissionRepository.findOne({
551
575
  where: {
552
576
  userId,
@@ -557,7 +581,6 @@ export class AuthenticationService {
557
581
  if (!companyPermission) {
558
582
  throw new UnauthorizedException('No access to this company');
559
583
  }
560
- // Get branch permissions
561
584
  const branchPermissions = await permissionRepository.find({
562
585
  where: {
563
586
  userId,
@@ -574,7 +597,7 @@ export class AuthenticationService {
574
597
  active: true
575
598
  }).orderBy('b.serial', 'ASC').addOrderBy('b.name', 'ASC').getMany();
576
599
  }
577
- /** Get company by ID */ async getCompanyById(companyId) {
600
+ async getCompanyById(companyId) {
578
601
  const companyRepository = await this.getCompanyRepository();
579
602
  if (!companyRepository) {
580
603
  return null;
@@ -585,7 +608,7 @@ export class AuthenticationService {
585
608
  }
586
609
  });
587
610
  }
588
- /** Get branch by ID */ async getBranchById(branchId) {
611
+ async getBranchById(branchId) {
589
612
  const branchRepository = await this.getBranchRepository();
590
613
  if (!branchRepository) {
591
614
  return null;
@@ -596,7 +619,7 @@ export class AuthenticationService {
596
619
  }
597
620
  });
598
621
  }
599
- /** Get user info for /auth/me endpoint. Returns user, company, and branch info (no tokens). */ async getUserInfo(userId, companyId, branchId) {
622
+ async getUserInfo(userId, companyId, branchId) {
600
623
  const userRepository = await this.getUserRepository();
601
624
  const user = await userRepository.findOne({
602
625
  where: {
@@ -622,7 +645,6 @@ export class AuthenticationService {
622
645
  }
623
646
  return response;
624
647
  }
625
- // Helper Methods
626
648
  ensureCompanyFeatureEnabled() {
627
649
  if (!this.isCompanyFeatureEnabled()) {
628
650
  throw new BadRequestException('Company feature is not enabled');
@@ -642,8 +664,7 @@ export class AuthenticationService {
642
664
  await userRepository.save(user);
643
665
  return user;
644
666
  }
645
- // Response Generators
646
- /** Create user info object */ toUserInfo(user) {
667
+ toUserInfo(user) {
647
668
  return {
648
669
  id: user.id,
649
670
  email: user.email,
@@ -652,7 +673,7 @@ export class AuthenticationService {
652
673
  profilePictureId: user.profilePictureId ?? null
653
674
  };
654
675
  }
655
- /** Create company info object */ toCompanyInfo(company) {
676
+ toCompanyInfo(company) {
656
677
  return {
657
678
  id: company.id,
658
679
  name: company.name,
@@ -660,7 +681,7 @@ export class AuthenticationService {
660
681
  logoId: company.logoId ?? null
661
682
  };
662
683
  }
663
- /** Create branch info object */ toBranchInfo(branch) {
684
+ toBranchInfo(branch) {
664
685
  return {
665
686
  id: branch.id,
666
687
  name: branch.name,
@@ -668,7 +689,7 @@ export class AuthenticationService {
668
689
  logoId: branch.logoId ?? null
669
690
  };
670
691
  }
671
- /** Generate JWT tokens */ generateTokens(payload) {
692
+ generateTokens(payload) {
672
693
  const accessToken = this.jwtService.sign({
673
694
  ...payload,
674
695
  type: 'access'
@@ -694,6 +715,7 @@ export class AuthenticationService {
694
715
  email: user.email,
695
716
  type: 'access'
696
717
  });
718
+ this.sendLoginNotification(user.id);
697
719
  return {
698
720
  accessToken,
699
721
  refreshToken,
@@ -708,6 +730,7 @@ export class AuthenticationService {
708
730
  companyId: company.id,
709
731
  branchId: branch?.id
710
732
  });
733
+ this.sendLoginNotification(user.id, company.id);
711
734
  return {
712
735
  accessToken,
713
736
  refreshToken,
@@ -716,6 +739,18 @@ export class AuthenticationService {
716
739
  branch: branch ? this.toBranchInfo(branch) : undefined
717
740
  };
718
741
  }
742
+ sendLoginNotification(userId, companyId) {
743
+ if (!this.notificationAdapter) return;
744
+ this.notificationAdapter.send({
745
+ userId,
746
+ title: 'Login successful',
747
+ message: 'You have successfully logged in to your account.',
748
+ type: NotificationType.INFO,
749
+ companyId
750
+ }).catch((error)=>{
751
+ this.logger.warn(`Failed to send login notification: ${error.message}`);
752
+ });
753
+ }
719
754
  async generateCompanySelectionResponse(user, companies, branches) {
720
755
  if (!this.sessionService) {
721
756
  throw new BadRequestException('Session service not available');
@@ -750,13 +785,15 @@ export class AuthenticationService {
750
785
  };
751
786
  }
752
787
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
753
- constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService){
788
+ constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, userEnricher, notificationAdapter){
754
789
  _define_property(this, "jwtService", void 0);
755
790
  _define_property(this, "authConfig", void 0);
756
791
  _define_property(this, "cacheManager", void 0);
757
792
  _define_property(this, "dataSourceProvider", void 0);
758
793
  _define_property(this, "sessionService", void 0);
759
794
  _define_property(this, "authEmailService", void 0);
795
+ _define_property(this, "userEnricher", void 0);
796
+ _define_property(this, "notificationAdapter", void 0);
760
797
  _define_property(this, "logger", void 0);
761
798
  this.jwtService = jwtService;
762
799
  this.authConfig = authConfig;
@@ -764,6 +801,8 @@ export class AuthenticationService {
764
801
  this.dataSourceProvider = dataSourceProvider;
765
802
  this.sessionService = sessionService;
766
803
  this.authEmailService = authEmailService;
804
+ this.userEnricher = userEnricher;
805
+ this.notificationAdapter = notificationAdapter;
767
806
  this.logger = new Logger(AuthenticationService.name);
768
807
  }
769
808
  }
@@ -781,6 +820,10 @@ AuthenticationService = _ts_decorate([
781
820
  _ts_param(4, Inject(CompanySelectionSessionService)),
782
821
  _ts_param(5, Optional()),
783
822
  _ts_param(5, Inject(AuthEmailService)),
823
+ _ts_param(6, Optional()),
824
+ _ts_param(6, Inject(USER_ENRICHER)),
825
+ _ts_param(7, Optional()),
826
+ _ts_param(7, Inject(NOTIFICATION_ADAPTER)),
784
827
  _ts_metadata("design:type", Function),
785
828
  _ts_metadata("design:paramtypes", [
786
829
  typeof JwtService === "undefined" ? Object : JwtService,
@@ -788,6 +831,8 @@ AuthenticationService = _ts_decorate([
788
831
  typeof HybridCache === "undefined" ? Object : HybridCache,
789
832
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
790
833
  typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
791
- typeof AuthEmailService === "undefined" ? Object : AuthEmailService
834
+ typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
835
+ typeof IUserEnricher === "undefined" ? Object : IUserEnricher,
836
+ typeof INotificationAdapter === "undefined" ? Object : INotificationAdapter
792
837
  ])
793
838
  ], AuthenticationService);