@flusys/nestjs-auth 1.1.0-beta → 2.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 (86) hide show
  1. package/README.md +590 -473
  2. package/cjs/config/auth.constants.js +8 -0
  3. package/cjs/controllers/authentication.controller.js +218 -39
  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 +68 -15
  9. package/cjs/docs/auth-swagger.config.js +14 -7
  10. package/cjs/dtos/authentication.dto.js +16 -43
  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/user-company-permission.entity.js +0 -6
  15. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  16. package/cjs/interceptors/set-token.interceptor.js +15 -55
  17. package/cjs/interfaces/auth-email-provider.interface.js +1 -1
  18. package/cjs/modules/auth.module.js +63 -147
  19. package/cjs/services/auth-config.service.js +22 -28
  20. package/cjs/services/auth-datasource.provider.js +9 -21
  21. package/cjs/services/auth-email.service.js +89 -85
  22. package/cjs/services/authentication.service.js +225 -396
  23. package/cjs/services/branch.service.js +9 -4
  24. package/cjs/services/company-selection-session.service.js +1 -1
  25. package/cjs/services/user-permission.service.js +43 -73
  26. package/cjs/services/user.service.js +78 -64
  27. package/cjs/strategies/jwt.strategy.js +44 -44
  28. package/cjs/validators/password.validator.js +45 -0
  29. package/config/auth.constants.d.ts +2 -0
  30. package/controllers/authentication.controller.d.ts +5 -3
  31. package/controllers/company-selection.controller.d.ts +6 -2
  32. package/controllers/user-permission.controller.d.ts +5 -4
  33. package/controllers/user.controller.d.ts +6 -0
  34. package/dtos/authentication.dto.d.ts +0 -4
  35. package/dtos/company-branch.dto.d.ts +0 -1
  36. package/dtos/company.dto.d.ts +0 -1
  37. package/dtos/index.d.ts +0 -1
  38. package/dtos/user-permission.dto.d.ts +4 -0
  39. package/dtos/user.dto.d.ts +1 -3
  40. package/entities/index.d.ts +3 -3
  41. package/entities/user-company-permission.entity.d.ts +0 -2
  42. package/fesm/config/auth.constants.js +6 -13
  43. package/fesm/controllers/authentication.controller.js +221 -42
  44. package/fesm/controllers/branch.controller.js +45 -2
  45. package/fesm/controllers/company-selection.controller.js +28 -21
  46. package/fesm/controllers/company.controller.js +45 -2
  47. package/fesm/controllers/user-permission.controller.js +84 -156
  48. package/fesm/controllers/user.controller.js +71 -18
  49. package/fesm/docs/auth-swagger.config.js +14 -7
  50. package/fesm/dtos/authentication.dto.js +17 -44
  51. package/fesm/dtos/index.js +0 -1
  52. package/fesm/dtos/user-permission.dto.js +28 -0
  53. package/fesm/dtos/user.dto.js +20 -5
  54. package/fesm/entities/user-company-permission.entity.js +0 -6
  55. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  56. package/fesm/interceptors/set-token.interceptor.js +14 -54
  57. package/fesm/interfaces/auth-email-provider.interface.js +1 -1
  58. package/fesm/modules/auth.module.js +64 -148
  59. package/fesm/services/auth-config.service.js +23 -29
  60. package/fesm/services/auth-datasource.provider.js +10 -22
  61. package/fesm/services/auth-email.service.js +90 -86
  62. package/fesm/services/authentication.service.js +225 -396
  63. package/fesm/services/branch.service.js +9 -4
  64. package/fesm/services/company-selection-session.service.js +1 -1
  65. package/fesm/services/user-permission.service.js +44 -74
  66. package/fesm/services/user.service.js +79 -65
  67. package/fesm/strategies/jwt.strategy.js +44 -44
  68. package/fesm/validators/password.validator.js +48 -0
  69. package/interceptors/clear-token.interceptor.d.ts +0 -1
  70. package/interceptors/set-token.interceptor.d.ts +1 -1
  71. package/interfaces/authentication.interface.d.ts +0 -4
  72. package/interfaces/user-enricher.interface.d.ts +1 -4
  73. package/modules/auth.module.d.ts +6 -3
  74. package/package.json +4 -4
  75. package/services/auth-config.service.d.ts +6 -8
  76. package/services/auth-datasource.provider.d.ts +2 -4
  77. package/services/auth-email.service.d.ts +8 -4
  78. package/services/authentication.service.d.ts +15 -16
  79. package/services/company-selection-session.service.d.ts +1 -1
  80. package/services/user-permission.service.d.ts +6 -9
  81. package/services/user.service.d.ts +3 -0
  82. package/strategies/jwt.strategy.d.ts +4 -1
  83. package/validators/password.validator.d.ts +10 -0
  84. package/cjs/dtos/user-company-permission.dto.js +0 -110
  85. package/dtos/user-company-permission.dto.d.ts +0 -10
  86. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -25,62 +25,59 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
+ import { HybridCache } from '@flusys/nestjs-shared/classes';
29
+ import { ErrorHandler, generateUniqueSlug } from '@flusys/nestjs-shared/utils';
28
30
  import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional, Scope, UnauthorizedException } from '@nestjs/common';
29
31
  import { JwtService } from '@nestjs/jwt';
30
- import { InjectRepository } from '@nestjs/typeorm';
31
32
  import * as bcrypt from 'bcrypt';
32
- import { Repository } from 'typeorm';
33
- import { ErrorHandler } from '@flusys/nestjs-shared/utils';
34
- import { AuthConfigService } from './auth-config.service';
35
- import { AuthDataSourceProvider } from './auth-datasource.provider';
36
- import { PermissionType } from '../enums';
37
33
  import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
38
- import { HybridCache } from '@flusys/nestjs-shared/classes';
39
- import { AuthEmailService } from './auth-email.service';
40
34
  import { CompanyBranch } from '../entities/company-branch.entity';
41
35
  import { Company } from '../entities/company.entity';
42
36
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
43
37
  import { User } from '../entities/user.entity';
38
+ import { PermissionType } from '../enums';
39
+ import { AuthConfigService } from './auth-config.service';
40
+ import { AuthDataSourceProvider } from './auth-datasource.provider';
41
+ import { AuthEmailService } from './auth-email.service';
44
42
  import { CompanySelectionSessionService } from './company-selection-session.service';
45
43
  export class AuthenticationService {
44
+ // Feature Flags
45
+ /** Check if company feature is enabled (tenant-aware with global fallback) */ isCompanyFeatureEnabled() {
46
+ return this.dataSourceProvider?.isCompanyFeatureEnabled() ?? this.authConfig.isCompanyFeatureEnabled();
47
+ }
46
48
  // Repository Getters
47
49
  async getUserRepository() {
48
- if (this.dataSourceProvider) {
49
- return this.dataSourceProvider.getRepository(User);
50
- }
51
- if (this.injectedUserRepository) {
52
- return this.injectedUserRepository;
50
+ if (!this.dataSourceProvider) {
51
+ throw new Error('User repository not available');
53
52
  }
54
- throw new Error('User repository not available');
53
+ return this.dataSourceProvider.getRepository(User);
55
54
  }
56
55
  async getPermissionRepository() {
57
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
58
- if (this.dataSourceProvider) {
59
- return this.dataSourceProvider.getRepository(UserCompanyPermission);
60
- }
61
- return this.injectedPermissionRepository ?? null;
56
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
57
+ return this.dataSourceProvider.getRepository(UserCompanyPermission);
62
58
  }
63
59
  async getCompanyRepository() {
64
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
65
- if (this.dataSourceProvider) {
66
- return this.dataSourceProvider.getRepository(Company);
67
- }
68
- return this.injectedCompanyRepository ?? null;
60
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
61
+ return this.dataSourceProvider.getRepository(Company);
69
62
  }
70
63
  async getBranchRepository() {
71
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
72
- if (this.dataSourceProvider) {
73
- return this.dataSourceProvider.getRepository(CompanyBranch);
74
- }
75
- return this.injectedBranchRepository ?? null;
64
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
65
+ return this.dataSourceProvider.getRepository(CompanyBranch);
76
66
  }
77
67
  // Login
78
- /** Login with email and password. Returns tokens or requires company/branch selection. */ async login(email, password) {
68
+ 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)) {
71
+ return {
72
+ requiresEmailVerification: true,
73
+ email,
74
+ message: 'Please verify your email address to complete registration.'
75
+ };
76
+ }
79
77
  const user = await this.validateUser(email, password);
80
78
  if (!user.isActive) {
81
79
  throw new UnauthorizedException('User account is deactivated');
82
80
  }
83
- // Check email verification if email service is enabled
84
81
  if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
85
82
  return {
86
83
  requiresEmailVerification: true,
@@ -88,7 +85,7 @@ export class AuthenticationService {
88
85
  message: 'Please verify your email address before logging in.'
89
86
  };
90
87
  }
91
- if (!this.authConfig.isCompanyFeatureEnabled()) {
88
+ if (!this.isCompanyFeatureEnabled()) {
92
89
  return this.generateLoginResponse(user);
93
90
  }
94
91
  return this.handleCompanyBasedLogin(user);
@@ -120,7 +117,7 @@ export class AuthenticationService {
120
117
  // If user has no company assignments yet, return simple login response
121
118
  // This allows users to login and be assigned to companies later
122
119
  if (companies.length === 0) {
123
- return this.generateLoginResponse(user, true);
120
+ return this.generateLoginResponse(user);
124
121
  }
125
122
  // Get branch permissions
126
123
  const branchPermissions = await permissionRepository.find({
@@ -151,12 +148,17 @@ export class AuthenticationService {
151
148
  email: dto.email
152
149
  }
153
150
  });
151
+ // When company feature is enabled and user exists, allow joining/creating company
152
+ if (existingUser && this.isCompanyFeatureEnabled()) {
153
+ return this.handleExistingUserCompanyRegistration(existingUser, dto);
154
+ }
155
+ // User exists but company feature is off - email must be unique
154
156
  if (existingUser) {
155
- throw new BadRequestException('Registration failed. Please try again or contact support.');
157
+ throw new BadRequestException('Registration failed due to email exists. Please try again or contact support.');
156
158
  }
157
159
  // If email verification is enabled, create pending registration instead of user
158
160
  if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
159
- const companyData = this.authConfig.isCompanyFeatureEnabled() ? {
161
+ const companyData = this.isCompanyFeatureEnabled() ? {
160
162
  companySlug: dto.companySlug,
161
163
  branchSlug: dto.branchSlug,
162
164
  newCompanyName: dto.newCompanyName,
@@ -179,6 +181,38 @@ export class AuthenticationService {
179
181
  // No email verification - create user directly
180
182
  return this.createUserDirectly(dto);
181
183
  }
184
+ /** Handle registration for existing user when company feature is enabled */ async handleExistingUserCompanyRegistration(existingUser, dto) {
185
+ if (!dto.companySlug && !dto.newCompanyName) {
186
+ throw new BadRequestException('Email already registered. Please login or provide a company to join/create.');
187
+ }
188
+ const permissionRepository = await this.getPermissionRepository();
189
+ const companyRepository = await this.getCompanyRepository();
190
+ if (!permissionRepository || !companyRepository) {
191
+ throw new BadRequestException('Company feature repositories not available');
192
+ }
193
+ // For existing users joining a company, check if already assigned
194
+ if (dto.companySlug) {
195
+ const company = await companyRepository.findOne({
196
+ where: {
197
+ slug: dto.companySlug,
198
+ isActive: true
199
+ }
200
+ });
201
+ if (company) {
202
+ const existingPermission = await permissionRepository.findOne({
203
+ where: {
204
+ userId: existingUser.id,
205
+ permissionType: PermissionType.COMPANY,
206
+ targetId: company.id
207
+ }
208
+ });
209
+ if (existingPermission) {
210
+ throw new BadRequestException('You are already assigned to this company. Please login.');
211
+ }
212
+ }
213
+ }
214
+ return this.processCompanyRegistration(existingUser, dto, true);
215
+ }
182
216
  /** Create user directly (no email verification) */ async createUserDirectly(dto) {
183
217
  const userRepository = await this.getUserRepository();
184
218
  const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
@@ -192,14 +226,13 @@ export class AuthenticationService {
192
226
  phoneVerified: false
193
227
  });
194
228
  await userRepository.save(user);
195
- if (!this.authConfig.isCompanyFeatureEnabled()) {
196
- return this.generateRegistrationResponse(user, false, false);
229
+ if (!this.isCompanyFeatureEnabled()) {
230
+ return this.generateRegistrationResponse(user);
197
231
  }
198
- return this.handleCompanyRegistration(user, dto, false);
232
+ return this.processCompanyRegistration(user, dto);
199
233
  }
200
234
  /** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
201
235
  const userRepository = await this.getUserRepository();
202
- // Check if user already exists (race condition protection)
203
236
  const existingUser = await userRepository.findOne({
204
237
  where: {
205
238
  email: pending.email
@@ -208,7 +241,6 @@ export class AuthenticationService {
208
241
  if (existingUser) {
209
242
  throw new BadRequestException('This email has already been registered.');
210
243
  }
211
- // Create user with verified email
212
244
  const user = userRepository.create({
213
245
  name: pending.name,
214
246
  email: pending.email,
@@ -219,107 +251,103 @@ export class AuthenticationService {
219
251
  phoneVerified: false
220
252
  });
221
253
  await userRepository.save(user);
222
- // Handle company feature if enabled
223
- if (this.authConfig.isCompanyFeatureEnabled() && pending.companyData) {
224
- return this.handleCompanyRegistrationFromPending(user, pending.companyData);
254
+ if (this.isCompanyFeatureEnabled() && pending.companyData) {
255
+ return this.processCompanyRegistration(user, pending.companyData);
225
256
  }
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);
257
+ return this.generateRegistrationResponse(user);
233
258
  }
234
- /** Shared logic for company registration (join existing or create new) */ async processCompanyRegistration(user, data, requiresEmailVerification) {
259
+ /** Process company registration (join existing or create new) */ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
235
260
  const companyRepository = await this.getCompanyRepository();
236
261
  const branchRepository = await this.getBranchRepository();
237
262
  const permissionRepository = await this.getPermissionRepository();
238
263
  if (!companyRepository || !branchRepository || !permissionRepository) {
239
- return this.generateRegistrationResponse(user, true, requiresEmailVerification);
264
+ if (throwOnUnavailable) throw new BadRequestException('Company feature repositories not available');
265
+ return this.generateRegistrationResponse(user);
240
266
  }
241
- let company = null;
242
- let branch = null;
267
+ const { company, branch } = await this.findOrCreateCompanyAndBranch(data, companyRepository, branchRepository);
268
+ if (company) {
269
+ await this.assignUserPermissions(user.id, company.id, branch?.id, permissionRepository);
270
+ return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined);
271
+ }
272
+ return this.generateRegistrationResponse(user);
273
+ }
274
+ /** Find existing company/branch or create new ones */ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
243
275
  if (data.companySlug) {
244
- company = await companyRepository.findOne({
276
+ const company = await companyRepo.findOne({
245
277
  where: {
246
278
  slug: data.companySlug,
247
279
  isActive: true
248
280
  }
249
281
  });
250
- if (!company) {
251
- throw new BadRequestException('Company not found or inactive');
252
- }
253
- if (data.branchSlug) {
254
- branch = await branchRepository.findOne({
255
- where: {
256
- slug: data.branchSlug,
257
- companyId: company.id,
258
- isActive: true
259
- }
260
- });
261
- if (!branch) {
262
- throw new BadRequestException('Branch not found or inactive');
282
+ if (!company) throw new BadRequestException('Company not found or inactive');
283
+ const branch = data.branchSlug ? await branchRepo.findOne({
284
+ where: {
285
+ slug: data.branchSlug,
286
+ companyId: company.id,
287
+ isActive: true
263
288
  }
264
- } else {
265
- branch = await branchRepository.findOne({
266
- where: {
267
- companyId: company.id,
268
- isActive: true
269
- },
270
- order: {
271
- serial: 'ASC',
272
- createdAt: 'ASC'
273
- }
274
- });
275
- }
276
- } else if (data.newCompanyName) {
277
- const slug = this.generateSlug(data.newCompanyName);
278
- const existingCompany = await companyRepository.findOne({
289
+ }) : await branchRepo.findOne({
279
290
  where: {
280
- slug
291
+ companyId: company.id,
292
+ isActive: true
293
+ },
294
+ order: {
295
+ serial: 'ASC',
296
+ createdAt: 'ASC'
281
297
  }
282
298
  });
283
- if (existingCompany) {
284
- throw new BadRequestException('Company creation failed. Please try a different name.');
285
- }
286
- company = companyRepository.create({
299
+ if (data.branchSlug && !branch) throw new BadRequestException('Branch not found or inactive');
300
+ return {
301
+ company,
302
+ branch
303
+ };
304
+ }
305
+ if (data.newCompanyName) {
306
+ const slug = await generateUniqueSlug(data.newCompanyName, async (baseSlug)=>{
307
+ const rows = await companyRepo.createQueryBuilder('c').select('c.slug').where('c.slug = :base OR c.slug LIKE :pattern', {
308
+ base: baseSlug,
309
+ pattern: `${baseSlug}-%`
310
+ }).getMany();
311
+ return rows.map((r)=>r.slug);
312
+ });
313
+ const company = companyRepo.create({
287
314
  name: data.newCompanyName,
288
315
  slug,
289
316
  phone: data.newCompanyPhone,
290
317
  address: data.newCompanyAddress,
291
318
  isActive: true
292
319
  });
293
- await companyRepository.save(company);
294
- branch = branchRepository.create({
320
+ await companyRepo.save(company);
321
+ const branch = branchRepo.create({
295
322
  name: 'Main Branch',
296
323
  slug: 'main-branch',
297
324
  companyId: company.id,
298
325
  isActive: true
299
326
  });
300
- await branchRepository.save(branch);
301
- }
302
- if (company) {
303
- const companyPermission = permissionRepository.create({
304
- userId: user.id,
305
- permissionType: PermissionType.COMPANY,
306
- targetId: company.id
307
- });
308
- await permissionRepository.save(companyPermission);
309
- if (branch) {
310
- const branchPermission = permissionRepository.create({
311
- userId: user.id,
312
- permissionType: PermissionType.BRANCH,
313
- targetId: branch.id
314
- });
315
- await permissionRepository.save(branchPermission);
316
- }
317
- return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
327
+ await branchRepo.save(branch);
328
+ return {
329
+ company,
330
+ branch
331
+ };
318
332
  }
319
- return this.generateRegistrationResponse(user, true, requiresEmailVerification);
333
+ return {
334
+ company: null,
335
+ branch: null
336
+ };
320
337
  }
321
- /** Generate URL-friendly slug from name */ generateSlug(name) {
322
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
338
+ /** Assign user to company and optionally branch */ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
339
+ await permissionRepo.save(permissionRepo.create({
340
+ userId,
341
+ permissionType: PermissionType.COMPANY,
342
+ targetId: companyId
343
+ }));
344
+ if (branchId) {
345
+ await permissionRepo.save(permissionRepo.create({
346
+ userId,
347
+ permissionType: PermissionType.BRANCH,
348
+ targetId: branchId
349
+ }));
350
+ }
323
351
  }
324
352
  // Company/Branch Selection
325
353
  /** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
@@ -331,66 +359,13 @@ export class AuthenticationService {
331
359
  if (!session) {
332
360
  throw new UnauthorizedException('Invalid or expired session');
333
361
  }
334
- const userRepository = await this.getUserRepository();
335
- const user = await userRepository.findOne({
336
- where: {
337
- id: session.userId
338
- }
339
- });
340
- if (!user) {
341
- throw new NotFoundException('User not found');
342
- }
343
- const permissionRepository = await this.getPermissionRepository();
344
- const companyRepository = await this.getCompanyRepository();
345
- const branchRepository = await this.getBranchRepository();
346
- if (!permissionRepository || !companyRepository || !branchRepository) {
347
- throw new BadRequestException('Repositories not available');
348
- }
349
- // Validate company access
350
- const companyPermission = await permissionRepository.findOne({
351
- where: {
352
- userId: user.id,
353
- permissionType: PermissionType.COMPANY,
354
- targetId: companyId
355
- }
356
- });
357
- if (!companyPermission) {
358
- throw new UnauthorizedException('No access to this company');
359
- }
360
- const company = await companyRepository.findOne({
361
- where: {
362
- id: companyId,
363
- isActive: true
364
- }
365
- });
366
- if (!company) {
367
- throw new UnauthorizedException('Company not found or inactive');
368
- }
369
- // Validate branch access
370
- const branchPermission = await permissionRepository.findOne({
371
- where: {
372
- userId: user.id,
373
- permissionType: PermissionType.BRANCH,
374
- targetId: branchId
375
- }
376
- });
377
- if (!branchPermission) {
378
- throw new UnauthorizedException('No access to this branch');
379
- }
380
- const branch = await branchRepository.findOne({
381
- where: {
382
- id: branchId,
383
- companyId,
384
- isActive: true
385
- }
386
- });
387
- if (!branch) {
388
- throw new UnauthorizedException('Branch not found or inactive');
389
- }
390
- return this.generateLoginResponseWithCompany(user, company, branch);
362
+ return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
391
363
  }
392
364
  /** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
393
365
  this.ensureCompanyFeatureEnabled();
366
+ return this.validateAndLoginWithCompany(userId, companyId, branchId);
367
+ }
368
+ /** Shared: Validate user access to company/branch and generate login response */ async validateAndLoginWithCompany(userId, companyId, branchId) {
394
369
  const userRepository = await this.getUserRepository();
395
370
  const user = await userRepository.findOne({
396
371
  where: {
@@ -407,14 +382,14 @@ export class AuthenticationService {
407
382
  throw new BadRequestException('Repositories not available');
408
383
  }
409
384
  // Validate company access
410
- const companyPermission = await permissionRepository.findOne({
385
+ const hasCompanyAccess = await permissionRepository.findOne({
411
386
  where: {
412
387
  userId,
413
388
  permissionType: PermissionType.COMPANY,
414
389
  targetId: companyId
415
390
  }
416
391
  });
417
- if (!companyPermission) {
392
+ if (!hasCompanyAccess) {
418
393
  throw new UnauthorizedException('No access to this company');
419
394
  }
420
395
  const company = await companyRepository.findOne({
@@ -426,14 +401,15 @@ export class AuthenticationService {
426
401
  if (!company) {
427
402
  throw new UnauthorizedException('Company not found or inactive');
428
403
  }
429
- const branchPermission = await permissionRepository.findOne({
404
+ // Validate branch access
405
+ const hasBranchAccess = await permissionRepository.findOne({
430
406
  where: {
431
407
  userId,
432
408
  permissionType: PermissionType.BRANCH,
433
409
  targetId: branchId
434
410
  }
435
411
  });
436
- if (!branchPermission) {
412
+ if (!hasBranchAccess) {
437
413
  throw new UnauthorizedException('No access to this branch');
438
414
  }
439
415
  const branch = await branchRepository.findOne({
@@ -454,6 +430,9 @@ export class AuthenticationService {
454
430
  const payload = this.jwtService.verify(refreshToken, {
455
431
  secret: this.authConfig.getRefreshTokenSecret()
456
432
  });
433
+ if (await this.isTokenRevoked(payload.id, payload.iat)) {
434
+ throw new UnauthorizedException('Token has been revoked');
435
+ }
457
436
  const userRepository = await this.getUserRepository();
458
437
  const user = await userRepository.findOne({
459
438
  where: {
@@ -463,7 +442,7 @@ export class AuthenticationService {
463
442
  if (!user || !user.isActive) {
464
443
  throw new UnauthorizedException('Invalid refresh token');
465
444
  }
466
- const accessPayload = {
445
+ return this.generateTokens({
467
446
  id: user.id,
468
447
  email: user.email,
469
448
  type: 'access',
@@ -473,44 +452,9 @@ export class AuthenticationService {
473
452
  },
474
453
  ...payload.branchId && {
475
454
  branchId: payload.branchId
476
- },
477
- ...payload.companyLogoId && {
478
- companyLogoId: payload.companyLogoId
479
- },
480
- ...payload.branchLogoId && {
481
- branchLogoId: payload.branchLogoId
482
- }
483
- };
484
- const refreshPayload = {
485
- id: user.id,
486
- email: user.email,
487
- type: 'refresh',
488
- profilePictureId: user.profilePictureId ?? undefined,
489
- ...payload.companyId && {
490
- companyId: payload.companyId
491
- },
492
- ...payload.branchId && {
493
- branchId: payload.branchId
494
- },
495
- ...payload.companyLogoId && {
496
- companyLogoId: payload.companyLogoId
497
- },
498
- ...payload.branchLogoId && {
499
- branchLogoId: payload.branchLogoId
500
455
  }
501
- };
502
- return {
503
- accessToken: this.jwtService.sign(accessPayload, {
504
- secret: this.authConfig.getJwtSecret(),
505
- expiresIn: this.authConfig.getJwtExpiration()
506
- }),
507
- refreshToken: this.jwtService.sign(refreshPayload, {
508
- secret: this.authConfig.getRefreshTokenSecret(),
509
- expiresIn: this.authConfig.getRefreshTokenExpiration()
510
- })
511
- };
456
+ });
512
457
  } catch (error) {
513
- // Log error details for debugging (not exposed to client)
514
458
  ErrorHandler.logError(this.logger, error, 'refreshToken');
515
459
  throw new UnauthorizedException('Invalid or expired refresh token');
516
460
  }
@@ -522,11 +466,8 @@ export class AuthenticationService {
522
466
  id: userId
523
467
  }
524
468
  });
525
- if (!user) {
526
- throw new UnauthorizedException('User not found');
527
- }
528
- const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
529
- if (!isPasswordValid) {
469
+ if (!user) throw new UnauthorizedException('User not found');
470
+ if (!await bcrypt.compare(currentPassword, user.password || '')) {
530
471
  throw new UnauthorizedException('Current password is incorrect');
531
472
  }
532
473
  user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
@@ -534,8 +475,9 @@ export class AuthenticationService {
534
475
  }
535
476
  /** Logout - Revokes all tokens. Stores revocation timestamp in cache. */ async logout(userId) {
536
477
  if (!this.cacheManager) {
537
- // Cache not available - graceful degradation (client-side logout only)
538
- return;
478
+ // Cache is required for secure logout - tokens cannot be revoked without it
479
+ 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
+ throw new BadRequestException('Logout service temporarily unavailable. Please try again later.');
539
481
  }
540
482
  const revokedAt = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
541
483
  const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
@@ -543,6 +485,7 @@ export class AuthenticationService {
543
485
  const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
544
486
  const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
545
487
  await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
488
+ this.logger.log(`User ${userId} logged out - all tokens revoked`);
546
489
  }
547
490
  /** Check if a token is revoked (issued before user's revocation timestamp) */ async isTokenRevoked(userId, tokenIssuedAt) {
548
491
  if (!this.cacheManager || !tokenIssuedAt) {
@@ -660,54 +603,28 @@ export class AuthenticationService {
660
603
  id: userId
661
604
  }
662
605
  });
663
- if (!user) {
664
- throw new UnauthorizedException('User not found');
665
- }
666
- // Build response (no tokens)
606
+ if (!user) throw new UnauthorizedException('User not found');
667
607
  const response = {
668
- user: {
669
- id: user.id,
670
- email: user.email,
671
- name: user.name ?? undefined,
672
- phone: user.phone ?? undefined,
673
- profilePictureId: user.profilePictureId ?? undefined
674
- }
608
+ user: this.toUserInfo(user)
675
609
  };
676
- // Add company/branch if available (when company feature is enabled)
677
- if (this.authConfig.isCompanyFeatureEnabled()) {
678
- // When company feature is enabled, companyId MUST be present
610
+ if (this.isCompanyFeatureEnabled()) {
679
611
  if (!companyId) {
680
- throw new UnauthorizedException('Company feature is enabled but user has no company assigned. Please select a company.');
612
+ throw new UnauthorizedException('Company feature is enabled but user has no company assigned.');
681
613
  }
682
614
  const company = await this.getCompanyById(companyId);
683
- if (!company) {
684
- throw new UnauthorizedException(`Company with ID ${companyId} not found. Please select a valid company.`);
685
- }
686
- response.company = {
687
- id: company.id,
688
- name: company.name,
689
- slug: company.slug,
690
- logoId: company.logoId ?? null
691
- };
692
- // Branch is optional in some configurations, but if provided it must exist
615
+ if (!company) throw new UnauthorizedException('Company not found.');
616
+ response.company = this.toCompanyInfo(company);
693
617
  if (branchId) {
694
618
  const branch = await this.getBranchById(branchId);
695
- if (!branch) {
696
- throw new UnauthorizedException(`Branch with ID ${branchId} not found. Please select a valid branch.`);
697
- }
698
- response.branch = {
699
- id: branch.id,
700
- name: branch.name,
701
- slug: branch.slug,
702
- logoId: branch.logoId ?? null
703
- };
619
+ if (!branch) throw new UnauthorizedException('Branch not found.');
620
+ response.branch = this.toBranchInfo(branch);
704
621
  }
705
622
  }
706
623
  return response;
707
624
  }
708
625
  // Helper Methods
709
626
  ensureCompanyFeatureEnabled() {
710
- if (!this.authConfig.isCompanyFeatureEnabled()) {
627
+ if (!this.isCompanyFeatureEnabled()) {
711
628
  throw new BadRequestException('Company feature is not enabled');
712
629
  }
713
630
  }
@@ -718,11 +635,7 @@ export class AuthenticationService {
718
635
  email
719
636
  }
720
637
  });
721
- if (!user) {
722
- throw new UnauthorizedException('Invalid credentials');
723
- }
724
- const isPasswordValid = await bcrypt.compare(password, user.password || '');
725
- if (!isPasswordValid) {
638
+ if (!user || !await bcrypt.compare(password, user.password || '')) {
726
639
  throw new UnauthorizedException('Invalid credentials');
727
640
  }
728
641
  user.lastLoginAt = new Date();
@@ -730,84 +643,77 @@ export class AuthenticationService {
730
643
  return user;
731
644
  }
732
645
  // Response Generators
733
- generateLoginResponse(user, companyFeatureEnabled = false) {
734
- const payload = {
646
+ /** Create user info object */ toUserInfo(user) {
647
+ return {
735
648
  id: user.id,
736
649
  email: user.email,
737
- type: 'access'
650
+ name: user.name ?? undefined,
651
+ phone: user.phone ?? null,
652
+ profilePictureId: user.profilePictureId ?? null
653
+ };
654
+ }
655
+ /** Create company info object */ toCompanyInfo(company) {
656
+ return {
657
+ id: company.id,
658
+ name: company.name,
659
+ slug: company.slug,
660
+ logoId: company.logoId ?? null
661
+ };
662
+ }
663
+ /** Create branch info object */ toBranchInfo(branch) {
664
+ return {
665
+ id: branch.id,
666
+ name: branch.name,
667
+ slug: branch.slug,
668
+ logoId: branch.logoId ?? null
738
669
  };
739
- const accessToken = this.jwtService.sign(payload, {
670
+ }
671
+ /** Generate JWT tokens */ generateTokens(payload) {
672
+ const accessToken = this.jwtService.sign({
673
+ ...payload,
674
+ type: 'access'
675
+ }, {
740
676
  secret: this.authConfig.getJwtSecret(),
741
677
  expiresIn: this.authConfig.getJwtExpiration()
742
678
  });
743
- const refreshPayload = {
744
- id: user.id,
745
- email: user.email,
679
+ const refreshToken = this.jwtService.sign({
680
+ ...payload,
746
681
  type: 'refresh'
747
- };
748
- const refreshToken = this.jwtService.sign(refreshPayload, {
682
+ }, {
749
683
  secret: this.authConfig.getRefreshTokenSecret(),
750
684
  expiresIn: this.authConfig.getRefreshTokenExpiration()
751
685
  });
752
686
  return {
753
687
  accessToken,
754
- refreshToken,
755
- user: {
756
- id: user.id,
757
- email: user.email,
758
- name: user.name ?? undefined,
759
- phone: user.phone ?? null,
760
- profilePictureId: user.profilePictureId ?? null
761
- },
762
- companyFeatureEnabled
688
+ refreshToken
763
689
  };
764
690
  }
765
- generateLoginResponseWithCompany(user, company, branch) {
766
- const payload = {
691
+ generateLoginResponse(user) {
692
+ const { accessToken, refreshToken } = this.generateTokens({
767
693
  id: user.id,
768
694
  email: user.email,
769
- type: 'access',
770
- companyId: company.id,
771
- branchId: branch?.id
772
- };
773
- const accessToken = this.jwtService.sign(payload, {
774
- secret: this.authConfig.getJwtSecret(),
775
- expiresIn: this.authConfig.getJwtExpiration()
695
+ type: 'access'
776
696
  });
777
- const refreshPayload = {
697
+ return {
698
+ accessToken,
699
+ refreshToken,
700
+ user: this.toUserInfo(user)
701
+ };
702
+ }
703
+ generateLoginResponseWithCompany(user, company, branch) {
704
+ const { accessToken, refreshToken } = this.generateTokens({
778
705
  id: user.id,
779
706
  email: user.email,
780
- type: 'refresh',
707
+ type: 'access',
781
708
  companyId: company.id,
782
709
  branchId: branch?.id
783
- };
784
- const refreshToken = this.jwtService.sign(refreshPayload, {
785
- secret: this.authConfig.getRefreshTokenSecret(),
786
- expiresIn: this.authConfig.getRefreshTokenExpiration()
787
710
  });
788
711
  return {
789
712
  accessToken,
790
713
  refreshToken,
791
- user: {
792
- id: user.id,
793
- email: user.email,
794
- name: user.name ?? undefined,
795
- phone: user.phone ?? null,
796
- profilePictureId: user.profilePictureId ?? null
797
- },
798
- company: {
799
- id: company.id,
800
- name: company.name,
801
- slug: company.slug,
802
- logoId: company.logoId ?? null
803
- },
804
- branch: branch ? {
805
- id: branch.id,
806
- name: branch.name,
807
- slug: branch.slug,
808
- logoId: branch.logoId ?? null
809
- } : undefined,
810
- companyFeatureEnabled: true
714
+ user: this.toUserInfo(user),
715
+ company: this.toCompanyInfo(company),
716
+ branch: branch ? this.toBranchInfo(branch) : undefined
811
717
  };
812
718
  }
813
719
  async generateCompanySelectionResponse(user, companies, branches) {
@@ -815,103 +721,42 @@ export class AuthenticationService {
815
721
  throw new BadRequestException('Session service not available');
816
722
  }
817
723
  const sessionId = await this.sessionService.createSession(user.id);
818
- // Build companies with their branches
819
724
  const companiesWithBranches = companies.map((company)=>({
820
- id: company.id,
821
- name: company.name,
822
- slug: company.slug,
725
+ ...this.toCompanyInfo(company),
823
726
  isActive: company.isActive,
824
- logoId: company.logoId ?? null,
825
727
  branches: branches.filter((b)=>b.companyId === company.id).map((b)=>({
826
- id: b.id,
827
- name: b.name,
828
- slug: b.slug,
829
- isActive: b.isActive,
830
- logoId: b.logoId ?? null
728
+ ...this.toBranchInfo(b),
729
+ isActive: b.isActive
831
730
  }))
832
731
  }));
833
732
  return {
834
733
  requiresSelection: true,
835
734
  sessionId,
836
735
  expiresAt: new Date(Date.now() + 5 * 60 * 1000),
837
- user: {
838
- id: user.id,
839
- email: user.email,
840
- name: user.name ?? undefined,
841
- phone: user.phone ?? null,
842
- profilePictureId: user.profilePictureId ?? null
843
- },
736
+ user: this.toUserInfo(user),
844
737
  companies: companiesWithBranches
845
738
  };
846
739
  }
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);
740
+ generateRegistrationResponse(user) {
863
741
  return {
864
- ...loginResponse,
742
+ ...this.generateLoginResponse(user),
865
743
  requiresEmailVerification: false
866
744
  };
867
745
  }
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);
746
+ generateRegistrationResponseWithCompany(user, company, branch) {
898
747
  return {
899
- ...loginResponse,
748
+ ...this.generateLoginResponseWithCompany(user, company, branch),
900
749
  requiresEmailVerification: false
901
750
  };
902
751
  }
903
752
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
904
- constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
753
+ constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService){
905
754
  _define_property(this, "jwtService", void 0);
906
755
  _define_property(this, "authConfig", void 0);
907
756
  _define_property(this, "cacheManager", void 0);
908
757
  _define_property(this, "dataSourceProvider", void 0);
909
758
  _define_property(this, "sessionService", void 0);
910
759
  _define_property(this, "authEmailService", void 0);
911
- _define_property(this, "injectedUserRepository", void 0);
912
- _define_property(this, "injectedPermissionRepository", void 0);
913
- _define_property(this, "injectedCompanyRepository", void 0);
914
- _define_property(this, "injectedBranchRepository", void 0);
915
760
  _define_property(this, "logger", void 0);
916
761
  this.jwtService = jwtService;
917
762
  this.authConfig = authConfig;
@@ -919,10 +764,6 @@ export class AuthenticationService {
919
764
  this.dataSourceProvider = dataSourceProvider;
920
765
  this.sessionService = sessionService;
921
766
  this.authEmailService = authEmailService;
922
- this.injectedUserRepository = injectedUserRepository;
923
- this.injectedPermissionRepository = injectedPermissionRepository;
924
- this.injectedCompanyRepository = injectedCompanyRepository;
925
- this.injectedBranchRepository = injectedBranchRepository;
926
767
  this.logger = new Logger(AuthenticationService.name);
927
768
  }
928
769
  }
@@ -940,14 +781,6 @@ AuthenticationService = _ts_decorate([
940
781
  _ts_param(4, Inject(CompanySelectionSessionService)),
941
782
  _ts_param(5, Optional()),
942
783
  _ts_param(5, Inject(AuthEmailService)),
943
- _ts_param(6, Optional()),
944
- _ts_param(6, InjectRepository(User)),
945
- _ts_param(7, Optional()),
946
- _ts_param(7, InjectRepository(UserCompanyPermission)),
947
- _ts_param(8, Optional()),
948
- _ts_param(8, InjectRepository(Company)),
949
- _ts_param(9, Optional()),
950
- _ts_param(9, InjectRepository(CompanyBranch)),
951
784
  _ts_metadata("design:type", Function),
952
785
  _ts_metadata("design:paramtypes", [
953
786
  typeof JwtService === "undefined" ? Object : JwtService,
@@ -955,10 +788,6 @@ AuthenticationService = _ts_decorate([
955
788
  typeof HybridCache === "undefined" ? Object : HybridCache,
956
789
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
957
790
  typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
958
- typeof AuthEmailService === "undefined" ? Object : AuthEmailService,
959
- typeof Repository === "undefined" ? Object : Repository,
960
- typeof Repository === "undefined" ? Object : Repository,
961
- typeof Repository === "undefined" ? Object : Repository,
962
- typeof Repository === "undefined" ? Object : Repository
791
+ typeof AuthEmailService === "undefined" ? Object : AuthEmailService
963
792
  ])
964
793
  ], AuthenticationService);