@flusys/nestjs-auth 1.0.0-rc → 1.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 (61) hide show
  1. package/README.md +530 -396
  2. package/cjs/controllers/authentication.controller.js +160 -28
  3. package/cjs/controllers/user-permission.controller.js +49 -136
  4. package/cjs/docs/auth-swagger.config.js +14 -7
  5. package/cjs/dtos/authentication.dto.js +0 -30
  6. package/cjs/dtos/index.js +0 -1
  7. package/cjs/entities/user-company-permission.entity.js +3 -3
  8. package/cjs/interceptors/clear-token.interceptor.js +8 -18
  9. package/cjs/interceptors/set-token.interceptor.js +8 -55
  10. package/cjs/interfaces/auth-email-provider.interface.js +1 -1
  11. package/cjs/modules/auth.module.js +63 -129
  12. package/cjs/services/auth-config.service.js +6 -29
  13. package/cjs/services/auth-datasource.provider.js +8 -20
  14. package/cjs/services/auth-email.service.js +72 -87
  15. package/cjs/services/authentication.service.js +211 -355
  16. package/cjs/services/branch.service.js +9 -5
  17. package/cjs/services/user-permission.service.js +43 -71
  18. package/cjs/services/user.service.js +18 -47
  19. package/cjs/strategies/jwt.strategy.js +44 -44
  20. package/controllers/authentication.controller.d.ts +5 -3
  21. package/controllers/user-permission.controller.d.ts +5 -4
  22. package/dtos/authentication.dto.d.ts +0 -4
  23. package/dtos/index.d.ts +0 -1
  24. package/entities/index.d.ts +3 -3
  25. package/entities/user-company-permission.entity.d.ts +1 -1
  26. package/fesm/controllers/authentication.controller.js +162 -30
  27. package/fesm/controllers/user-permission.controller.js +50 -137
  28. package/fesm/docs/auth-swagger.config.js +14 -7
  29. package/fesm/dtos/authentication.dto.js +0 -30
  30. package/fesm/dtos/index.js +0 -1
  31. package/fesm/entities/user-company-permission.entity.js +3 -3
  32. package/fesm/interceptors/clear-token.interceptor.js +8 -18
  33. package/fesm/interceptors/set-token.interceptor.js +8 -55
  34. package/fesm/interfaces/auth-email-provider.interface.js +1 -1
  35. package/fesm/modules/auth.module.js +63 -129
  36. package/fesm/services/auth-config.service.js +6 -29
  37. package/fesm/services/auth-datasource.provider.js +8 -20
  38. package/fesm/services/auth-email.service.js +72 -87
  39. package/fesm/services/authentication.service.js +211 -355
  40. package/fesm/services/branch.service.js +9 -5
  41. package/fesm/services/user-permission.service.js +44 -72
  42. package/fesm/services/user.service.js +18 -47
  43. package/fesm/strategies/jwt.strategy.js +44 -44
  44. package/interceptors/clear-token.interceptor.d.ts +0 -1
  45. package/interceptors/set-token.interceptor.d.ts +1 -2
  46. package/interfaces/authentication.interface.d.ts +0 -4
  47. package/modules/auth.module.d.ts +6 -2
  48. package/package.json +3 -3
  49. package/services/auth-config.service.d.ts +2 -7
  50. package/services/auth-datasource.provider.d.ts +2 -4
  51. package/services/auth-email.service.d.ts +7 -4
  52. package/services/authentication.service.d.ts +14 -8
  53. package/services/user-permission.service.d.ts +6 -8
  54. package/services/user.service.d.ts +1 -1
  55. package/strategies/jwt.strategy.d.ts +4 -1
  56. package/cjs/dtos/user-company-permission.dto.js +0 -110
  57. package/cjs/guards/auth-throttler.guard.js +0 -53
  58. package/dtos/user-company-permission.dto.d.ts +0 -10
  59. package/fesm/dtos/user-company-permission.dto.js +0 -98
  60. package/fesm/guards/auth-throttler.guard.js +0 -43
  61. package/guards/auth-throttler.guard.d.ts +0 -7
@@ -25,22 +25,26 @@ 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
32
  import * as bcrypt from 'bcrypt';
31
- import { ErrorHandler } from '@flusys/nestjs-shared/utils';
32
- import { AuthConfigService } from './auth-config.service';
33
- import { AuthDataSourceProvider } from './auth-datasource.provider';
34
- import { PermissionType } from '../enums';
35
33
  import { BCRYPT_SALT_ROUNDS, TOKEN_REVOKED_PREFIX } from '../config';
36
- import { HybridCache } from '@flusys/nestjs-shared/classes';
37
- import { AuthEmailService } from './auth-email.service';
38
34
  import { CompanyBranch } from '../entities/company-branch.entity';
39
35
  import { Company } from '../entities/company.entity';
40
36
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
41
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';
42
42
  import { CompanySelectionSessionService } from './company-selection-session.service';
43
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
+ }
44
48
  // Repository Getters
45
49
  async getUserRepository() {
46
50
  if (!this.dataSourceProvider) {
@@ -49,24 +53,31 @@ export class AuthenticationService {
49
53
  return this.dataSourceProvider.getRepository(User);
50
54
  }
51
55
  async getPermissionRepository() {
52
- if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
56
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
53
57
  return this.dataSourceProvider.getRepository(UserCompanyPermission);
54
58
  }
55
59
  async getCompanyRepository() {
56
- if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
60
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
57
61
  return this.dataSourceProvider.getRepository(Company);
58
62
  }
59
63
  async getBranchRepository() {
60
- if (!this.authConfig.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
64
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
61
65
  return this.dataSourceProvider.getRepository(CompanyBranch);
62
66
  }
63
67
  // Login
64
- /** 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
+ }
65
77
  const user = await this.validateUser(email, password);
66
78
  if (!user.isActive) {
67
79
  throw new UnauthorizedException('User account is deactivated');
68
80
  }
69
- // Check email verification if email service is enabled
70
81
  if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
71
82
  return {
72
83
  requiresEmailVerification: true,
@@ -74,7 +85,7 @@ export class AuthenticationService {
74
85
  message: 'Please verify your email address before logging in.'
75
86
  };
76
87
  }
77
- if (!this.authConfig.isCompanyFeatureEnabled()) {
88
+ if (!this.isCompanyFeatureEnabled()) {
78
89
  return this.generateLoginResponse(user);
79
90
  }
80
91
  return this.handleCompanyBasedLogin(user);
@@ -106,7 +117,7 @@ export class AuthenticationService {
106
117
  // If user has no company assignments yet, return simple login response
107
118
  // This allows users to login and be assigned to companies later
108
119
  if (companies.length === 0) {
109
- return this.generateLoginResponse(user, true);
120
+ return this.generateLoginResponse(user);
110
121
  }
111
122
  // Get branch permissions
112
123
  const branchPermissions = await permissionRepository.find({
@@ -137,12 +148,17 @@ export class AuthenticationService {
137
148
  email: dto.email
138
149
  }
139
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
140
156
  if (existingUser) {
141
- 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.');
142
158
  }
143
159
  // If email verification is enabled, create pending registration instead of user
144
160
  if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
145
- const companyData = this.authConfig.isCompanyFeatureEnabled() ? {
161
+ const companyData = this.isCompanyFeatureEnabled() ? {
146
162
  companySlug: dto.companySlug,
147
163
  branchSlug: dto.branchSlug,
148
164
  newCompanyName: dto.newCompanyName,
@@ -165,6 +181,38 @@ export class AuthenticationService {
165
181
  // No email verification - create user directly
166
182
  return this.createUserDirectly(dto);
167
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
+ }
168
216
  /** Create user directly (no email verification) */ async createUserDirectly(dto) {
169
217
  const userRepository = await this.getUserRepository();
170
218
  const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
@@ -178,14 +226,13 @@ export class AuthenticationService {
178
226
  phoneVerified: false
179
227
  });
180
228
  await userRepository.save(user);
181
- if (!this.authConfig.isCompanyFeatureEnabled()) {
182
- return this.generateRegistrationResponse(user, false, false);
229
+ if (!this.isCompanyFeatureEnabled()) {
230
+ return this.generateRegistrationResponse(user);
183
231
  }
184
- return this.handleCompanyRegistration(user, dto, false);
232
+ return this.processCompanyRegistration(user, dto);
185
233
  }
186
234
  /** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
187
235
  const userRepository = await this.getUserRepository();
188
- // Check if user already exists (race condition protection)
189
236
  const existingUser = await userRepository.findOne({
190
237
  where: {
191
238
  email: pending.email
@@ -194,7 +241,6 @@ export class AuthenticationService {
194
241
  if (existingUser) {
195
242
  throw new BadRequestException('This email has already been registered.');
196
243
  }
197
- // Create user with verified email
198
244
  const user = userRepository.create({
199
245
  name: pending.name,
200
246
  email: pending.email,
@@ -205,107 +251,103 @@ export class AuthenticationService {
205
251
  phoneVerified: false
206
252
  });
207
253
  await userRepository.save(user);
208
- // Handle company feature if enabled
209
- if (this.authConfig.isCompanyFeatureEnabled() && pending.companyData) {
210
- return this.handleCompanyRegistrationFromPending(user, pending.companyData);
254
+ if (this.isCompanyFeatureEnabled() && pending.companyData) {
255
+ return this.processCompanyRegistration(user, pending.companyData);
211
256
  }
212
- return this.generateRegistrationResponse(user, this.authConfig.isCompanyFeatureEnabled(), false);
213
- }
214
- /** Handle company registration from pending registration data */ async handleCompanyRegistrationFromPending(user, companyData) {
215
- return this.processCompanyRegistration(user, companyData, false);
257
+ return this.generateRegistrationResponse(user);
216
258
  }
217
- /** Handle company-related registration logic */ async handleCompanyRegistration(user, dto, requiresEmailVerification) {
218
- return this.processCompanyRegistration(user, dto, requiresEmailVerification);
219
- }
220
- /** 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) {
221
260
  const companyRepository = await this.getCompanyRepository();
222
261
  const branchRepository = await this.getBranchRepository();
223
262
  const permissionRepository = await this.getPermissionRepository();
224
263
  if (!companyRepository || !branchRepository || !permissionRepository) {
225
- return this.generateRegistrationResponse(user, true, requiresEmailVerification);
264
+ if (throwOnUnavailable) throw new BadRequestException('Company feature repositories not available');
265
+ return this.generateRegistrationResponse(user);
266
+ }
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);
226
271
  }
227
- let company = null;
228
- let branch = null;
272
+ return this.generateRegistrationResponse(user);
273
+ }
274
+ /** Find existing company/branch or create new ones */ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
229
275
  if (data.companySlug) {
230
- company = await companyRepository.findOne({
276
+ const company = await companyRepo.findOne({
231
277
  where: {
232
278
  slug: data.companySlug,
233
279
  isActive: true
234
280
  }
235
281
  });
236
- if (!company) {
237
- throw new BadRequestException('Company not found or inactive');
238
- }
239
- if (data.branchSlug) {
240
- branch = await branchRepository.findOne({
241
- where: {
242
- slug: data.branchSlug,
243
- companyId: company.id,
244
- isActive: true
245
- }
246
- });
247
- if (!branch) {
248
- 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
249
288
  }
250
- } else {
251
- branch = await branchRepository.findOne({
252
- where: {
253
- companyId: company.id,
254
- isActive: true
255
- },
256
- order: {
257
- serial: 'ASC',
258
- createdAt: 'ASC'
259
- }
260
- });
261
- }
262
- } else if (data.newCompanyName) {
263
- const slug = this.generateSlug(data.newCompanyName);
264
- const existingCompany = await companyRepository.findOne({
289
+ }) : await branchRepo.findOne({
265
290
  where: {
266
- slug
291
+ companyId: company.id,
292
+ isActive: true
293
+ },
294
+ order: {
295
+ serial: 'ASC',
296
+ createdAt: 'ASC'
267
297
  }
268
298
  });
269
- if (existingCompany) {
270
- throw new BadRequestException('Company creation failed. Please try a different name.');
271
- }
272
- 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({
273
314
  name: data.newCompanyName,
274
315
  slug,
275
316
  phone: data.newCompanyPhone,
276
317
  address: data.newCompanyAddress,
277
318
  isActive: true
278
319
  });
279
- await companyRepository.save(company);
280
- branch = branchRepository.create({
320
+ await companyRepo.save(company);
321
+ const branch = branchRepo.create({
281
322
  name: 'Main Branch',
282
323
  slug: 'main-branch',
283
324
  companyId: company.id,
284
325
  isActive: true
285
326
  });
286
- await branchRepository.save(branch);
287
- }
288
- if (company) {
289
- const companyPermission = permissionRepository.create({
290
- userId: user.id,
291
- permissionType: PermissionType.COMPANY,
292
- targetId: company.id
293
- });
294
- await permissionRepository.save(companyPermission);
295
- if (branch) {
296
- const branchPermission = permissionRepository.create({
297
- userId: user.id,
298
- permissionType: PermissionType.BRANCH,
299
- targetId: branch.id
300
- });
301
- await permissionRepository.save(branchPermission);
302
- }
303
- return this.generateRegistrationResponseWithCompany(user, company, branch ?? undefined, requiresEmailVerification);
327
+ await branchRepo.save(branch);
328
+ return {
329
+ company,
330
+ branch
331
+ };
304
332
  }
305
- return this.generateRegistrationResponse(user, true, requiresEmailVerification);
333
+ return {
334
+ company: null,
335
+ branch: null
336
+ };
306
337
  }
307
- /** Generate URL-friendly slug from name */ generateSlug(name) {
308
- 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
+ }
309
351
  }
310
352
  // Company/Branch Selection
311
353
  /** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
@@ -317,66 +359,13 @@ export class AuthenticationService {
317
359
  if (!session) {
318
360
  throw new UnauthorizedException('Invalid or expired session');
319
361
  }
320
- const userRepository = await this.getUserRepository();
321
- const user = await userRepository.findOne({
322
- where: {
323
- id: session.userId
324
- }
325
- });
326
- if (!user) {
327
- throw new NotFoundException('User not found');
328
- }
329
- const permissionRepository = await this.getPermissionRepository();
330
- const companyRepository = await this.getCompanyRepository();
331
- const branchRepository = await this.getBranchRepository();
332
- if (!permissionRepository || !companyRepository || !branchRepository) {
333
- throw new BadRequestException('Repositories not available');
334
- }
335
- // Validate company access
336
- const companyPermission = await permissionRepository.findOne({
337
- where: {
338
- userId: user.id,
339
- permissionType: PermissionType.COMPANY,
340
- targetId: companyId
341
- }
342
- });
343
- if (!companyPermission) {
344
- throw new UnauthorizedException('No access to this company');
345
- }
346
- const company = await companyRepository.findOne({
347
- where: {
348
- id: companyId,
349
- isActive: true
350
- }
351
- });
352
- if (!company) {
353
- throw new UnauthorizedException('Company not found or inactive');
354
- }
355
- // Validate branch access
356
- const branchPermission = await permissionRepository.findOne({
357
- where: {
358
- userId: user.id,
359
- permissionType: PermissionType.BRANCH,
360
- targetId: branchId
361
- }
362
- });
363
- if (!branchPermission) {
364
- throw new UnauthorizedException('No access to this branch');
365
- }
366
- const branch = await branchRepository.findOne({
367
- where: {
368
- id: branchId,
369
- companyId,
370
- isActive: true
371
- }
372
- });
373
- if (!branch) {
374
- throw new UnauthorizedException('Branch not found or inactive');
375
- }
376
- return this.generateLoginResponseWithCompany(user, company, branch);
362
+ return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
377
363
  }
378
364
  /** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
379
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) {
380
369
  const userRepository = await this.getUserRepository();
381
370
  const user = await userRepository.findOne({
382
371
  where: {
@@ -393,14 +382,14 @@ export class AuthenticationService {
393
382
  throw new BadRequestException('Repositories not available');
394
383
  }
395
384
  // Validate company access
396
- const companyPermission = await permissionRepository.findOne({
385
+ const hasCompanyAccess = await permissionRepository.findOne({
397
386
  where: {
398
387
  userId,
399
388
  permissionType: PermissionType.COMPANY,
400
389
  targetId: companyId
401
390
  }
402
391
  });
403
- if (!companyPermission) {
392
+ if (!hasCompanyAccess) {
404
393
  throw new UnauthorizedException('No access to this company');
405
394
  }
406
395
  const company = await companyRepository.findOne({
@@ -412,14 +401,15 @@ export class AuthenticationService {
412
401
  if (!company) {
413
402
  throw new UnauthorizedException('Company not found or inactive');
414
403
  }
415
- const branchPermission = await permissionRepository.findOne({
404
+ // Validate branch access
405
+ const hasBranchAccess = await permissionRepository.findOne({
416
406
  where: {
417
407
  userId,
418
408
  permissionType: PermissionType.BRANCH,
419
409
  targetId: branchId
420
410
  }
421
411
  });
422
- if (!branchPermission) {
412
+ if (!hasBranchAccess) {
423
413
  throw new UnauthorizedException('No access to this branch');
424
414
  }
425
415
  const branch = await branchRepository.findOne({
@@ -440,9 +430,7 @@ export class AuthenticationService {
440
430
  const payload = this.jwtService.verify(refreshToken, {
441
431
  secret: this.authConfig.getRefreshTokenSecret()
442
432
  });
443
- // Check if token has been revoked (user logged out after token was issued)
444
- const isRevoked = await this.isTokenRevoked(payload.id, payload.iat);
445
- if (isRevoked) {
433
+ if (await this.isTokenRevoked(payload.id, payload.iat)) {
446
434
  throw new UnauthorizedException('Token has been revoked');
447
435
  }
448
436
  const userRepository = await this.getUserRepository();
@@ -454,7 +442,7 @@ export class AuthenticationService {
454
442
  if (!user || !user.isActive) {
455
443
  throw new UnauthorizedException('Invalid refresh token');
456
444
  }
457
- const accessPayload = {
445
+ return this.generateTokens({
458
446
  id: user.id,
459
447
  email: user.email,
460
448
  type: 'access',
@@ -464,44 +452,9 @@ export class AuthenticationService {
464
452
  },
465
453
  ...payload.branchId && {
466
454
  branchId: payload.branchId
467
- },
468
- ...payload.companyLogoId && {
469
- companyLogoId: payload.companyLogoId
470
- },
471
- ...payload.branchLogoId && {
472
- branchLogoId: payload.branchLogoId
473
- }
474
- };
475
- const refreshPayload = {
476
- id: user.id,
477
- email: user.email,
478
- type: 'refresh',
479
- profilePictureId: user.profilePictureId ?? undefined,
480
- ...payload.companyId && {
481
- companyId: payload.companyId
482
- },
483
- ...payload.branchId && {
484
- branchId: payload.branchId
485
- },
486
- ...payload.companyLogoId && {
487
- companyLogoId: payload.companyLogoId
488
- },
489
- ...payload.branchLogoId && {
490
- branchLogoId: payload.branchLogoId
491
455
  }
492
- };
493
- return {
494
- accessToken: this.jwtService.sign(accessPayload, {
495
- secret: this.authConfig.getJwtSecret(),
496
- expiresIn: this.authConfig.getJwtExpiration()
497
- }),
498
- refreshToken: this.jwtService.sign(refreshPayload, {
499
- secret: this.authConfig.getRefreshTokenSecret(),
500
- expiresIn: this.authConfig.getRefreshTokenExpiration()
501
- })
502
- };
456
+ });
503
457
  } catch (error) {
504
- // Log error details for debugging (not exposed to client)
505
458
  ErrorHandler.logError(this.logger, error, 'refreshToken');
506
459
  throw new UnauthorizedException('Invalid or expired refresh token');
507
460
  }
@@ -513,11 +466,8 @@ export class AuthenticationService {
513
466
  id: userId
514
467
  }
515
468
  });
516
- if (!user) {
517
- throw new UnauthorizedException('User not found');
518
- }
519
- const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
520
- if (!isPasswordValid) {
469
+ if (!user) throw new UnauthorizedException('User not found');
470
+ if (!await bcrypt.compare(currentPassword, user.password || '')) {
521
471
  throw new UnauthorizedException('Current password is incorrect');
522
472
  }
523
473
  user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
@@ -653,54 +603,28 @@ export class AuthenticationService {
653
603
  id: userId
654
604
  }
655
605
  });
656
- if (!user) {
657
- throw new UnauthorizedException('User not found');
658
- }
659
- // Build response (no tokens)
606
+ if (!user) throw new UnauthorizedException('User not found');
660
607
  const response = {
661
- user: {
662
- id: user.id,
663
- email: user.email,
664
- name: user.name ?? undefined,
665
- phone: user.phone ?? undefined,
666
- profilePictureId: user.profilePictureId ?? undefined
667
- }
608
+ user: this.toUserInfo(user)
668
609
  };
669
- // Add company/branch if available (when company feature is enabled)
670
- if (this.authConfig.isCompanyFeatureEnabled()) {
671
- // When company feature is enabled, companyId MUST be present
610
+ if (this.isCompanyFeatureEnabled()) {
672
611
  if (!companyId) {
673
- 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.');
674
613
  }
675
614
  const company = await this.getCompanyById(companyId);
676
- if (!company) {
677
- throw new UnauthorizedException(`Company with ID ${companyId} not found. Please select a valid company.`);
678
- }
679
- response.company = {
680
- id: company.id,
681
- name: company.name,
682
- slug: company.slug,
683
- logoId: company.logoId ?? null
684
- };
685
- // 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);
686
617
  if (branchId) {
687
618
  const branch = await this.getBranchById(branchId);
688
- if (!branch) {
689
- throw new UnauthorizedException(`Branch with ID ${branchId} not found. Please select a valid branch.`);
690
- }
691
- response.branch = {
692
- id: branch.id,
693
- name: branch.name,
694
- slug: branch.slug,
695
- logoId: branch.logoId ?? null
696
- };
619
+ if (!branch) throw new UnauthorizedException('Branch not found.');
620
+ response.branch = this.toBranchInfo(branch);
697
621
  }
698
622
  }
699
623
  return response;
700
624
  }
701
625
  // Helper Methods
702
626
  ensureCompanyFeatureEnabled() {
703
- if (!this.authConfig.isCompanyFeatureEnabled()) {
627
+ if (!this.isCompanyFeatureEnabled()) {
704
628
  throw new BadRequestException('Company feature is not enabled');
705
629
  }
706
630
  }
@@ -711,11 +635,7 @@ export class AuthenticationService {
711
635
  email
712
636
  }
713
637
  });
714
- if (!user) {
715
- throw new UnauthorizedException('Invalid credentials');
716
- }
717
- const isPasswordValid = await bcrypt.compare(password, user.password || '');
718
- if (!isPasswordValid) {
638
+ if (!user || !await bcrypt.compare(password, user.password || '')) {
719
639
  throw new UnauthorizedException('Invalid credentials');
720
640
  }
721
641
  user.lastLoginAt = new Date();
@@ -723,84 +643,77 @@ export class AuthenticationService {
723
643
  return user;
724
644
  }
725
645
  // Response Generators
726
- generateLoginResponse(user, companyFeatureEnabled = false) {
727
- const payload = {
646
+ /** Create user info object */ toUserInfo(user) {
647
+ return {
728
648
  id: user.id,
729
649
  email: user.email,
730
- 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
731
669
  };
732
- 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
+ }, {
733
676
  secret: this.authConfig.getJwtSecret(),
734
677
  expiresIn: this.authConfig.getJwtExpiration()
735
678
  });
736
- const refreshPayload = {
737
- id: user.id,
738
- email: user.email,
679
+ const refreshToken = this.jwtService.sign({
680
+ ...payload,
739
681
  type: 'refresh'
740
- };
741
- const refreshToken = this.jwtService.sign(refreshPayload, {
682
+ }, {
742
683
  secret: this.authConfig.getRefreshTokenSecret(),
743
684
  expiresIn: this.authConfig.getRefreshTokenExpiration()
744
685
  });
745
686
  return {
746
687
  accessToken,
747
- refreshToken,
748
- user: {
749
- id: user.id,
750
- email: user.email,
751
- name: user.name ?? undefined,
752
- phone: user.phone ?? null,
753
- profilePictureId: user.profilePictureId ?? null
754
- },
755
- companyFeatureEnabled
688
+ refreshToken
756
689
  };
757
690
  }
758
- generateLoginResponseWithCompany(user, company, branch) {
759
- const payload = {
691
+ generateLoginResponse(user) {
692
+ const { accessToken, refreshToken } = this.generateTokens({
760
693
  id: user.id,
761
694
  email: user.email,
762
- type: 'access',
763
- companyId: company.id,
764
- branchId: branch?.id
765
- };
766
- const accessToken = this.jwtService.sign(payload, {
767
- secret: this.authConfig.getJwtSecret(),
768
- expiresIn: this.authConfig.getJwtExpiration()
695
+ type: 'access'
769
696
  });
770
- 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({
771
705
  id: user.id,
772
706
  email: user.email,
773
- type: 'refresh',
707
+ type: 'access',
774
708
  companyId: company.id,
775
709
  branchId: branch?.id
776
- };
777
- const refreshToken = this.jwtService.sign(refreshPayload, {
778
- secret: this.authConfig.getRefreshTokenSecret(),
779
- expiresIn: this.authConfig.getRefreshTokenExpiration()
780
710
  });
781
711
  return {
782
712
  accessToken,
783
713
  refreshToken,
784
- user: {
785
- id: user.id,
786
- email: user.email,
787
- name: user.name ?? undefined,
788
- phone: user.phone ?? null,
789
- profilePictureId: user.profilePictureId ?? null
790
- },
791
- company: {
792
- id: company.id,
793
- name: company.name,
794
- slug: company.slug,
795
- logoId: company.logoId ?? null
796
- },
797
- branch: branch ? {
798
- id: branch.id,
799
- name: branch.name,
800
- slug: branch.slug,
801
- logoId: branch.logoId ?? null
802
- } : undefined,
803
- companyFeatureEnabled: true
714
+ user: this.toUserInfo(user),
715
+ company: this.toCompanyInfo(company),
716
+ branch: branch ? this.toBranchInfo(branch) : undefined
804
717
  };
805
718
  }
806
719
  async generateCompanySelectionResponse(user, companies, branches) {
@@ -808,88 +721,31 @@ export class AuthenticationService {
808
721
  throw new BadRequestException('Session service not available');
809
722
  }
810
723
  const sessionId = await this.sessionService.createSession(user.id);
811
- // Build companies with their branches
812
724
  const companiesWithBranches = companies.map((company)=>({
813
- id: company.id,
814
- name: company.name,
815
- slug: company.slug,
725
+ ...this.toCompanyInfo(company),
816
726
  isActive: company.isActive,
817
- logoId: company.logoId ?? null,
818
727
  branches: branches.filter((b)=>b.companyId === company.id).map((b)=>({
819
- id: b.id,
820
- name: b.name,
821
- slug: b.slug,
822
- isActive: b.isActive,
823
- logoId: b.logoId ?? null
728
+ ...this.toBranchInfo(b),
729
+ isActive: b.isActive
824
730
  }))
825
731
  }));
826
732
  return {
827
733
  requiresSelection: true,
828
734
  sessionId,
829
735
  expiresAt: new Date(Date.now() + 5 * 60 * 1000),
830
- user: {
831
- id: user.id,
832
- email: user.email,
833
- name: user.name ?? undefined,
834
- phone: user.phone ?? null,
835
- profilePictureId: user.profilePictureId ?? null
836
- },
736
+ user: this.toUserInfo(user),
837
737
  companies: companiesWithBranches
838
738
  };
839
739
  }
840
- /** Generate registration response - tokens included only if email verification not required */ generateRegistrationResponse(user, companyFeatureEnabled, requiresEmailVerification) {
841
- const userInfo = {
842
- id: user.id,
843
- email: user.email,
844
- name: user.name ?? undefined,
845
- phone: user.phone ?? null,
846
- profilePictureId: user.profilePictureId ?? null
847
- };
848
- if (requiresEmailVerification) {
849
- return {
850
- user: userInfo,
851
- companyFeatureEnabled,
852
- requiresEmailVerification: true
853
- };
854
- }
855
- const loginResponse = this.generateLoginResponse(user, companyFeatureEnabled);
740
+ generateRegistrationResponse(user) {
856
741
  return {
857
- ...loginResponse,
742
+ ...this.generateLoginResponse(user),
858
743
  requiresEmailVerification: false
859
744
  };
860
745
  }
861
- /** Generate registration response with company - tokens included only if email verification not required */ generateRegistrationResponseWithCompany(user, company, branch, requiresEmailVerification = false) {
862
- const userInfo = {
863
- id: user.id,
864
- email: user.email,
865
- name: user.name ?? undefined,
866
- phone: user.phone ?? null,
867
- profilePictureId: user.profilePictureId ?? null
868
- };
869
- const companyInfo = {
870
- id: company.id,
871
- name: company.name,
872
- slug: company.slug,
873
- logoId: company.logoId ?? null
874
- };
875
- const branchInfo = branch ? {
876
- id: branch.id,
877
- name: branch.name,
878
- slug: branch.slug,
879
- logoId: branch.logoId ?? null
880
- } : undefined;
881
- if (requiresEmailVerification) {
882
- return {
883
- user: userInfo,
884
- company: companyInfo,
885
- branch: branchInfo,
886
- companyFeatureEnabled: true,
887
- requiresEmailVerification: true
888
- };
889
- }
890
- const loginResponse = this.generateLoginResponseWithCompany(user, company, branch);
746
+ generateRegistrationResponseWithCompany(user, company, branch) {
891
747
  return {
892
- ...loginResponse,
748
+ ...this.generateLoginResponseWithCompany(user, company, branch),
893
749
  requiresEmailVerification: false
894
750
  };
895
751
  }