@flusys/nestjs-auth 1.0.0-beta → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +590 -473
  2. package/cjs/config/auth.constants.js +12 -0
  3. package/cjs/controllers/authentication.controller.js +365 -24
  4. package/cjs/controllers/branch.controller.js +44 -1
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +44 -1
  7. package/cjs/controllers/user-permission.controller.js +82 -154
  8. package/cjs/controllers/user.controller.js +211 -7
  9. package/cjs/docs/auth-swagger.config.js +14 -7
  10. package/cjs/dtos/authentication.dto.js +114 -38
  11. package/cjs/dtos/index.js +0 -1
  12. package/cjs/dtos/user-permission.dto.js +28 -0
  13. package/cjs/dtos/user.dto.js +19 -4
  14. package/cjs/entities/index.js +5 -1
  15. package/cjs/entities/pending-registration.entity.js +125 -0
  16. package/cjs/entities/user-company-permission.entity.js +3 -9
  17. package/cjs/entities/user-token.entity.js +129 -0
  18. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  19. package/cjs/interceptors/set-token.interceptor.js +15 -55
  20. package/cjs/interfaces/auth-email-provider.interface.js +45 -0
  21. package/cjs/interfaces/index.js +2 -0
  22. package/cjs/interfaces/user-enricher.interface.js +4 -0
  23. package/cjs/modules/auth.module.js +65 -143
  24. package/cjs/services/auth-config.service.js +25 -27
  25. package/cjs/services/auth-datasource.provider.js +13 -23
  26. package/cjs/services/auth-email.service.js +456 -0
  27. package/cjs/services/authentication.service.js +297 -349
  28. package/cjs/services/branch.service.js +9 -4
  29. package/cjs/services/company-selection-session.service.js +1 -1
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/services/user-permission.service.js +43 -73
  32. package/cjs/services/user.service.js +184 -34
  33. package/cjs/strategies/jwt.strategy.js +44 -44
  34. package/cjs/validators/password.validator.js +45 -0
  35. package/config/auth.constants.d.ts +3 -0
  36. package/controllers/authentication.controller.d.ts +16 -10
  37. package/controllers/company-selection.controller.d.ts +6 -2
  38. package/controllers/user-permission.controller.d.ts +5 -4
  39. package/controllers/user.controller.d.ts +33 -0
  40. package/dtos/authentication.dto.d.ts +20 -6
  41. package/dtos/company-branch.dto.d.ts +0 -1
  42. package/dtos/company.dto.d.ts +0 -1
  43. package/dtos/index.d.ts +0 -1
  44. package/dtos/user-permission.dto.d.ts +4 -0
  45. package/dtos/user.dto.d.ts +1 -2
  46. package/entities/index.d.ts +7 -3
  47. package/entities/pending-registration.entity.d.ts +17 -0
  48. package/entities/user-company-permission.entity.d.ts +1 -3
  49. package/entities/user-token.entity.d.ts +15 -0
  50. package/fesm/config/auth.constants.js +6 -2
  51. package/fesm/controllers/authentication.controller.js +368 -27
  52. package/fesm/controllers/branch.controller.js +45 -2
  53. package/fesm/controllers/company-selection.controller.js +28 -21
  54. package/fesm/controllers/company.controller.js +45 -2
  55. package/fesm/controllers/user-permission.controller.js +84 -156
  56. package/fesm/controllers/user.controller.js +214 -10
  57. package/fesm/docs/auth-swagger.config.js +14 -7
  58. package/fesm/dtos/authentication.dto.js +101 -39
  59. package/fesm/dtos/index.js +0 -1
  60. package/fesm/dtos/user-permission.dto.js +28 -0
  61. package/fesm/dtos/user.dto.js +20 -5
  62. package/fesm/entities/index.js +7 -1
  63. package/fesm/entities/pending-registration.entity.js +115 -0
  64. package/fesm/entities/user-company-permission.entity.js +3 -9
  65. package/fesm/entities/user-token.entity.js +113 -0
  66. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  67. package/fesm/interceptors/set-token.interceptor.js +14 -54
  68. package/fesm/interfaces/auth-email-provider.interface.js +38 -0
  69. package/fesm/interfaces/authentication.interface.js +1 -1
  70. package/fesm/interfaces/index.js +2 -0
  71. package/fesm/interfaces/user-enricher.interface.js +26 -0
  72. package/fesm/modules/auth.module.js +67 -145
  73. package/fesm/services/auth-config.service.js +26 -28
  74. package/fesm/services/auth-datasource.provider.js +14 -24
  75. package/fesm/services/auth-email.service.js +405 -0
  76. package/fesm/services/authentication.service.js +297 -349
  77. package/fesm/services/branch.service.js +9 -4
  78. package/fesm/services/company-selection-session.service.js +1 -1
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/services/user-permission.service.js +44 -74
  81. package/fesm/services/user.service.js +186 -36
  82. package/fesm/strategies/jwt.strategy.js +44 -44
  83. package/fesm/validators/password.validator.js +48 -0
  84. package/interceptors/clear-token.interceptor.d.ts +0 -1
  85. package/interceptors/set-token.interceptor.d.ts +1 -1
  86. package/interfaces/auth-email-provider.interface.d.ts +6 -0
  87. package/interfaces/auth-module-options.interface.d.ts +4 -1
  88. package/interfaces/authentication.interface.d.ts +13 -3
  89. package/interfaces/index.d.ts +2 -0
  90. package/interfaces/user-enricher.interface.d.ts +64 -0
  91. package/interfaces/user.interface.d.ts +1 -0
  92. package/modules/auth.module.d.ts +6 -3
  93. package/package.json +4 -4
  94. package/services/auth-config.service.d.ts +7 -8
  95. package/services/auth-datasource.provider.d.ts +2 -4
  96. package/services/auth-email.service.d.ts +54 -0
  97. package/services/authentication.service.d.ts +27 -16
  98. package/services/company-selection-session.service.d.ts +1 -1
  99. package/services/index.d.ts +1 -0
  100. package/services/user-permission.service.d.ts +6 -9
  101. package/services/user.service.d.ts +19 -3
  102. package/strategies/jwt.strategy.d.ts +4 -1
  103. package/validators/password.validator.d.ts +10 -0
  104. package/cjs/dtos/user-company-permission.dto.js +0 -110
  105. package/dtos/user-company-permission.dto.d.ts +0 -10
  106. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -25,62 +25,67 @@ 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
34
  import { CompanyBranch } from '../entities/company-branch.entity';
40
35
  import { Company } from '../entities/company.entity';
41
36
  import { UserCompanyPermission } from '../entities/user-company-permission.entity';
42
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';
43
42
  import { CompanySelectionSessionService } from './company-selection-session.service';
44
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
+ }
45
48
  // Repository Getters
46
49
  async getUserRepository() {
47
- if (this.dataSourceProvider) {
48
- return this.dataSourceProvider.getRepository(User);
49
- }
50
- if (this.injectedUserRepository) {
51
- return this.injectedUserRepository;
50
+ if (!this.dataSourceProvider) {
51
+ throw new Error('User repository not available');
52
52
  }
53
- throw new Error('User repository not available');
53
+ return this.dataSourceProvider.getRepository(User);
54
54
  }
55
55
  async getPermissionRepository() {
56
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
57
- if (this.dataSourceProvider) {
58
- return this.dataSourceProvider.getRepository(UserCompanyPermission);
59
- }
60
- return this.injectedPermissionRepository ?? null;
56
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
57
+ return this.dataSourceProvider.getRepository(UserCompanyPermission);
61
58
  }
62
59
  async getCompanyRepository() {
63
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
64
- if (this.dataSourceProvider) {
65
- return this.dataSourceProvider.getRepository(Company);
66
- }
67
- return this.injectedCompanyRepository ?? null;
60
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
61
+ return this.dataSourceProvider.getRepository(Company);
68
62
  }
69
63
  async getBranchRepository() {
70
- if (!this.authConfig.isCompanyFeatureEnabled()) return null;
71
- if (this.dataSourceProvider) {
72
- return this.dataSourceProvider.getRepository(CompanyBranch);
73
- }
74
- return this.injectedBranchRepository ?? null;
64
+ if (!this.isCompanyFeatureEnabled() || !this.dataSourceProvider) return null;
65
+ return this.dataSourceProvider.getRepository(CompanyBranch);
75
66
  }
76
67
  // Login
77
- /** 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
+ }
78
77
  const user = await this.validateUser(email, password);
79
78
  if (!user.isActive) {
80
79
  throw new UnauthorizedException('User account is deactivated');
81
80
  }
82
- // If company feature not enabled, return simple token
83
- if (!this.authConfig.isCompanyFeatureEnabled()) {
81
+ if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
82
+ return {
83
+ requiresEmailVerification: true,
84
+ email: user.email,
85
+ message: 'Please verify your email address before logging in.'
86
+ };
87
+ }
88
+ if (!this.isCompanyFeatureEnabled()) {
84
89
  return this.generateLoginResponse(user);
85
90
  }
86
91
  return this.handleCompanyBasedLogin(user);
@@ -112,7 +117,7 @@ export class AuthenticationService {
112
117
  // If user has no company assignments yet, return simple login response
113
118
  // This allows users to login and be assigned to companies later
114
119
  if (companies.length === 0) {
115
- return this.generateLoginResponse(user, true);
120
+ return this.generateLoginResponse(user);
116
121
  }
117
122
  // Get branch permissions
118
123
  const branchPermissions = await permissionRepository.find({
@@ -136,19 +141,80 @@ export class AuthenticationService {
136
141
  return await this.generateCompanySelectionResponse(user, companies, branches);
137
142
  }
138
143
  // Registration
139
- /** Register a new user. If company feature enabled, optionally join/create company. */ async register(dto) {
144
+ /** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
140
145
  const userRepository = await this.getUserRepository();
141
- // Check if user exists
142
- // Note: Generic error message to prevent email enumeration attacks
143
146
  const existingUser = await userRepository.findOne({
144
147
  where: {
145
148
  email: dto.email
146
149
  }
147
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
148
156
  if (existingUser) {
149
- 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.');
158
+ }
159
+ // If email verification is enabled, create pending registration instead of user
160
+ if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
161
+ const companyData = this.isCompanyFeatureEnabled() ? {
162
+ companySlug: dto.companySlug,
163
+ branchSlug: dto.branchSlug,
164
+ newCompanyName: dto.newCompanyName,
165
+ newCompanyPhone: dto.newCompanyPhone,
166
+ newCompanyAddress: dto.newCompanyAddress
167
+ } : undefined;
168
+ const result = await this.authEmailService.createPendingRegistration({
169
+ email: dto.email,
170
+ name: dto.name,
171
+ phone: dto.phone,
172
+ password: dto.password,
173
+ companyData,
174
+ verifyUrlBase
175
+ });
176
+ return {
177
+ requiresEmailVerification: true,
178
+ message: result.message
179
+ };
180
+ }
181
+ // No email verification - create user directly
182
+ return this.createUserDirectly(dto);
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');
150
192
  }
151
- // Create user
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
+ }
216
+ /** Create user directly (no email verification) */ async createUserDirectly(dto) {
217
+ const userRepository = await this.getUserRepository();
152
218
  const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
153
219
  const user = userRepository.create({
154
220
  name: dto.name,
@@ -160,112 +226,128 @@ export class AuthenticationService {
160
226
  phoneVerified: false
161
227
  });
162
228
  await userRepository.save(user);
163
- // If company feature not enabled, return simple response
164
- if (!this.authConfig.isCompanyFeatureEnabled()) {
165
- return this.generateLoginResponse(user, false);
229
+ if (!this.isCompanyFeatureEnabled()) {
230
+ return this.generateRegistrationResponse(user);
231
+ }
232
+ return this.processCompanyRegistration(user, dto);
233
+ }
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
239
+ }
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);
166
256
  }
167
- // Handle company feature
168
- return this.handleCompanyRegistration(user, dto);
257
+ return this.generateRegistrationResponse(user);
169
258
  }
170
- /** Handle company-related registration logic */ async handleCompanyRegistration(user, dto) {
259
+ /** Process company registration (join existing or create new) */ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
171
260
  const companyRepository = await this.getCompanyRepository();
172
261
  const branchRepository = await this.getBranchRepository();
173
262
  const permissionRepository = await this.getPermissionRepository();
174
263
  if (!companyRepository || !branchRepository || !permissionRepository) {
175
- return this.generateLoginResponse(user, true);
264
+ if (throwOnUnavailable) throw new BadRequestException('Company feature repositories not available');
265
+ return this.generateRegistrationResponse(user);
176
266
  }
177
- let company = null;
178
- let branch = null;
179
- // Case 1: Join existing company by slug
180
- if (dto.companySlug) {
181
- company = await companyRepository.findOne({
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) {
275
+ if (data.companySlug) {
276
+ const company = await companyRepo.findOne({
182
277
  where: {
183
- slug: dto.companySlug,
278
+ slug: data.companySlug,
184
279
  isActive: true
185
280
  }
186
281
  });
187
- if (!company) {
188
- throw new BadRequestException('Company not found or inactive');
189
- }
190
- // Find branch by slug or get default
191
- if (dto.branchSlug) {
192
- branch = await branchRepository.findOne({
193
- where: {
194
- slug: dto.branchSlug,
195
- companyId: company.id,
196
- isActive: true
197
- }
198
- });
199
- if (!branch) {
200
- 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
201
288
  }
202
- } else {
203
- // Get first active branch
204
- branch = await branchRepository.findOne({
205
- where: {
206
- companyId: company.id,
207
- isActive: true
208
- },
209
- order: {
210
- serial: 'ASC',
211
- createdAt: 'ASC'
212
- }
213
- });
214
- }
215
- } else if (dto.newCompanyName) {
216
- const slug = this.generateSlug(dto.newCompanyName);
217
- // Check if slug exists
218
- const existingCompany = await companyRepository.findOne({
289
+ }) : await branchRepo.findOne({
219
290
  where: {
220
- slug
291
+ companyId: company.id,
292
+ isActive: true
293
+ },
294
+ order: {
295
+ serial: 'ASC',
296
+ createdAt: 'ASC'
221
297
  }
222
298
  });
223
- if (existingCompany) {
224
- throw new BadRequestException('Company creation failed. Please try a different name.');
225
- }
226
- // Create company
227
- company = companyRepository.create({
228
- name: dto.newCompanyName,
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({
314
+ name: data.newCompanyName,
229
315
  slug,
230
- phone: dto.newCompanyPhone,
231
- address: dto.newCompanyAddress,
316
+ phone: data.newCompanyPhone,
317
+ address: data.newCompanyAddress,
232
318
  isActive: true
233
319
  });
234
- await companyRepository.save(company);
235
- // Create default branch
236
- branch = branchRepository.create({
320
+ await companyRepo.save(company);
321
+ const branch = branchRepo.create({
237
322
  name: 'Main Branch',
238
323
  slug: 'main-branch',
239
324
  companyId: company.id,
240
325
  isActive: true
241
326
  });
242
- await branchRepository.save(branch);
243
- }
244
- // Create permissions if company exists
245
- if (company) {
246
- // Create company permission
247
- const companyPermission = permissionRepository.create({
248
- userId: user.id,
249
- permissionType: PermissionType.COMPANY,
250
- targetId: company.id
251
- });
252
- await permissionRepository.save(companyPermission);
253
- // Create branch permission if branch exists
254
- if (branch) {
255
- const branchPermission = permissionRepository.create({
256
- userId: user.id,
257
- permissionType: PermissionType.BRANCH,
258
- targetId: branch.id
259
- });
260
- await permissionRepository.save(branchPermission);
261
- }
262
- return this.generateLoginResponseWithCompany(user, company, branch ?? undefined);
327
+ await branchRepo.save(branch);
328
+ return {
329
+ company,
330
+ branch
331
+ };
263
332
  }
264
- // No company assigned
265
- return this.generateLoginResponse(user, true);
333
+ return {
334
+ company: null,
335
+ branch: null
336
+ };
266
337
  }
267
- /** Generate URL-friendly slug from name */ generateSlug(name) {
268
- 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
+ }
269
351
  }
270
352
  // Company/Branch Selection
271
353
  /** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
@@ -277,66 +359,13 @@ export class AuthenticationService {
277
359
  if (!session) {
278
360
  throw new UnauthorizedException('Invalid or expired session');
279
361
  }
280
- const userRepository = await this.getUserRepository();
281
- const user = await userRepository.findOne({
282
- where: {
283
- id: session.userId
284
- }
285
- });
286
- if (!user) {
287
- throw new NotFoundException('User not found');
288
- }
289
- const permissionRepository = await this.getPermissionRepository();
290
- const companyRepository = await this.getCompanyRepository();
291
- const branchRepository = await this.getBranchRepository();
292
- if (!permissionRepository || !companyRepository || !branchRepository) {
293
- throw new BadRequestException('Repositories not available');
294
- }
295
- // Validate company access
296
- const companyPermission = await permissionRepository.findOne({
297
- where: {
298
- userId: user.id,
299
- permissionType: PermissionType.COMPANY,
300
- targetId: companyId
301
- }
302
- });
303
- if (!companyPermission) {
304
- throw new UnauthorizedException('No access to this company');
305
- }
306
- const company = await companyRepository.findOne({
307
- where: {
308
- id: companyId,
309
- isActive: true
310
- }
311
- });
312
- if (!company) {
313
- throw new UnauthorizedException('Company not found or inactive');
314
- }
315
- // Validate branch access
316
- const branchPermission = await permissionRepository.findOne({
317
- where: {
318
- userId: user.id,
319
- permissionType: PermissionType.BRANCH,
320
- targetId: branchId
321
- }
322
- });
323
- if (!branchPermission) {
324
- throw new UnauthorizedException('No access to this branch');
325
- }
326
- const branch = await branchRepository.findOne({
327
- where: {
328
- id: branchId,
329
- companyId,
330
- isActive: true
331
- }
332
- });
333
- if (!branch) {
334
- throw new UnauthorizedException('Branch not found or inactive');
335
- }
336
- return this.generateLoginResponseWithCompany(user, company, branch);
362
+ return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
337
363
  }
338
364
  /** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
339
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) {
340
369
  const userRepository = await this.getUserRepository();
341
370
  const user = await userRepository.findOne({
342
371
  where: {
@@ -353,14 +382,14 @@ export class AuthenticationService {
353
382
  throw new BadRequestException('Repositories not available');
354
383
  }
355
384
  // Validate company access
356
- const companyPermission = await permissionRepository.findOne({
385
+ const hasCompanyAccess = await permissionRepository.findOne({
357
386
  where: {
358
387
  userId,
359
388
  permissionType: PermissionType.COMPANY,
360
389
  targetId: companyId
361
390
  }
362
391
  });
363
- if (!companyPermission) {
392
+ if (!hasCompanyAccess) {
364
393
  throw new UnauthorizedException('No access to this company');
365
394
  }
366
395
  const company = await companyRepository.findOne({
@@ -372,14 +401,15 @@ export class AuthenticationService {
372
401
  if (!company) {
373
402
  throw new UnauthorizedException('Company not found or inactive');
374
403
  }
375
- const branchPermission = await permissionRepository.findOne({
404
+ // Validate branch access
405
+ const hasBranchAccess = await permissionRepository.findOne({
376
406
  where: {
377
407
  userId,
378
408
  permissionType: PermissionType.BRANCH,
379
409
  targetId: branchId
380
410
  }
381
411
  });
382
- if (!branchPermission) {
412
+ if (!hasBranchAccess) {
383
413
  throw new UnauthorizedException('No access to this branch');
384
414
  }
385
415
  const branch = await branchRepository.findOne({
@@ -400,6 +430,9 @@ export class AuthenticationService {
400
430
  const payload = this.jwtService.verify(refreshToken, {
401
431
  secret: this.authConfig.getRefreshTokenSecret()
402
432
  });
433
+ if (await this.isTokenRevoked(payload.id, payload.iat)) {
434
+ throw new UnauthorizedException('Token has been revoked');
435
+ }
403
436
  const userRepository = await this.getUserRepository();
404
437
  const user = await userRepository.findOne({
405
438
  where: {
@@ -409,7 +442,7 @@ export class AuthenticationService {
409
442
  if (!user || !user.isActive) {
410
443
  throw new UnauthorizedException('Invalid refresh token');
411
444
  }
412
- const accessPayload = {
445
+ return this.generateTokens({
413
446
  id: user.id,
414
447
  email: user.email,
415
448
  type: 'access',
@@ -419,44 +452,9 @@ export class AuthenticationService {
419
452
  },
420
453
  ...payload.branchId && {
421
454
  branchId: payload.branchId
422
- },
423
- ...payload.companyLogoId && {
424
- companyLogoId: payload.companyLogoId
425
- },
426
- ...payload.branchLogoId && {
427
- branchLogoId: payload.branchLogoId
428
455
  }
429
- };
430
- const refreshPayload = {
431
- id: user.id,
432
- email: user.email,
433
- type: 'refresh',
434
- profilePictureId: user.profilePictureId ?? undefined,
435
- ...payload.companyId && {
436
- companyId: payload.companyId
437
- },
438
- ...payload.branchId && {
439
- branchId: payload.branchId
440
- },
441
- ...payload.companyLogoId && {
442
- companyLogoId: payload.companyLogoId
443
- },
444
- ...payload.branchLogoId && {
445
- branchLogoId: payload.branchLogoId
446
- }
447
- };
448
- return {
449
- accessToken: this.jwtService.sign(accessPayload, {
450
- secret: this.authConfig.getJwtSecret(),
451
- expiresIn: this.authConfig.getJwtExpiration()
452
- }),
453
- refreshToken: this.jwtService.sign(refreshPayload, {
454
- secret: this.authConfig.getRefreshTokenSecret(),
455
- expiresIn: this.authConfig.getRefreshTokenExpiration()
456
- })
457
- };
456
+ });
458
457
  } catch (error) {
459
- // Log error details for debugging (not exposed to client)
460
458
  ErrorHandler.logError(this.logger, error, 'refreshToken');
461
459
  throw new UnauthorizedException('Invalid or expired refresh token');
462
460
  }
@@ -468,11 +466,8 @@ export class AuthenticationService {
468
466
  id: userId
469
467
  }
470
468
  });
471
- if (!user) {
472
- throw new UnauthorizedException('User not found');
473
- }
474
- const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
475
- if (!isPasswordValid) {
469
+ if (!user) throw new UnauthorizedException('User not found');
470
+ if (!await bcrypt.compare(currentPassword, user.password || '')) {
476
471
  throw new UnauthorizedException('Current password is incorrect');
477
472
  }
478
473
  user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
@@ -480,8 +475,9 @@ export class AuthenticationService {
480
475
  }
481
476
  /** Logout - Revokes all tokens. Stores revocation timestamp in cache. */ async logout(userId) {
482
477
  if (!this.cacheManager) {
483
- // Cache not available - graceful degradation (client-side logout only)
484
- 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.');
485
481
  }
486
482
  const revokedAt = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
487
483
  const cacheKey = `${TOKEN_REVOKED_PREFIX}:${userId}`;
@@ -489,6 +485,7 @@ export class AuthenticationService {
489
485
  const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
490
486
  const ttlMs = this.parseExpiryToMs(refreshTokenExpiry);
491
487
  await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
488
+ this.logger.log(`User ${userId} logged out - all tokens revoked`);
492
489
  }
493
490
  /** Check if a token is revoked (issued before user's revocation timestamp) */ async isTokenRevoked(userId, tokenIssuedAt) {
494
491
  if (!this.cacheManager || !tokenIssuedAt) {
@@ -606,53 +603,28 @@ export class AuthenticationService {
606
603
  id: userId
607
604
  }
608
605
  });
609
- if (!user) {
610
- throw new UnauthorizedException('User not found');
611
- }
612
- // Build response (no tokens)
606
+ if (!user) throw new UnauthorizedException('User not found');
613
607
  const response = {
614
- user: {
615
- id: user.id,
616
- email: user.email,
617
- name: user.name ?? undefined,
618
- profilePictureId: user.profilePictureId ?? undefined
619
- }
608
+ user: this.toUserInfo(user)
620
609
  };
621
- // Add company/branch if available (when company feature is enabled)
622
- if (this.authConfig.isCompanyFeatureEnabled()) {
623
- // When company feature is enabled, companyId MUST be present
610
+ if (this.isCompanyFeatureEnabled()) {
624
611
  if (!companyId) {
625
- 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.');
626
613
  }
627
614
  const company = await this.getCompanyById(companyId);
628
- if (!company) {
629
- throw new UnauthorizedException(`Company with ID ${companyId} not found. Please select a valid company.`);
630
- }
631
- response.company = {
632
- id: company.id,
633
- name: company.name,
634
- slug: company.slug,
635
- logoId: company.logoId ?? null
636
- };
637
- // 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);
638
617
  if (branchId) {
639
618
  const branch = await this.getBranchById(branchId);
640
- if (!branch) {
641
- throw new UnauthorizedException(`Branch with ID ${branchId} not found. Please select a valid branch.`);
642
- }
643
- response.branch = {
644
- id: branch.id,
645
- name: branch.name,
646
- slug: branch.slug,
647
- logoId: branch.logoId ?? null
648
- };
619
+ if (!branch) throw new UnauthorizedException('Branch not found.');
620
+ response.branch = this.toBranchInfo(branch);
649
621
  }
650
622
  }
651
623
  return response;
652
624
  }
653
625
  // Helper Methods
654
626
  ensureCompanyFeatureEnabled() {
655
- if (!this.authConfig.isCompanyFeatureEnabled()) {
627
+ if (!this.isCompanyFeatureEnabled()) {
656
628
  throw new BadRequestException('Company feature is not enabled');
657
629
  }
658
630
  }
@@ -663,11 +635,7 @@ export class AuthenticationService {
663
635
  email
664
636
  }
665
637
  });
666
- if (!user) {
667
- throw new UnauthorizedException('Invalid credentials');
668
- }
669
- const isPasswordValid = await bcrypt.compare(password, user.password || '');
670
- if (!isPasswordValid) {
638
+ if (!user || !await bcrypt.compare(password, user.password || '')) {
671
639
  throw new UnauthorizedException('Invalid credentials');
672
640
  }
673
641
  user.lastLoginAt = new Date();
@@ -675,82 +643,77 @@ export class AuthenticationService {
675
643
  return user;
676
644
  }
677
645
  // Response Generators
678
- generateLoginResponse(user, companyFeatureEnabled = false) {
679
- const payload = {
646
+ /** Create user info object */ toUserInfo(user) {
647
+ return {
680
648
  id: user.id,
681
649
  email: user.email,
682
- type: 'access'
650
+ name: user.name ?? undefined,
651
+ phone: user.phone ?? null,
652
+ profilePictureId: user.profilePictureId ?? null
683
653
  };
684
- const accessToken = this.jwtService.sign(payload, {
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
669
+ };
670
+ }
671
+ /** Generate JWT tokens */ generateTokens(payload) {
672
+ const accessToken = this.jwtService.sign({
673
+ ...payload,
674
+ type: 'access'
675
+ }, {
685
676
  secret: this.authConfig.getJwtSecret(),
686
677
  expiresIn: this.authConfig.getJwtExpiration()
687
678
  });
688
- const refreshPayload = {
689
- id: user.id,
690
- email: user.email,
679
+ const refreshToken = this.jwtService.sign({
680
+ ...payload,
691
681
  type: 'refresh'
692
- };
693
- const refreshToken = this.jwtService.sign(refreshPayload, {
682
+ }, {
694
683
  secret: this.authConfig.getRefreshTokenSecret(),
695
684
  expiresIn: this.authConfig.getRefreshTokenExpiration()
696
685
  });
697
686
  return {
698
687
  accessToken,
699
- refreshToken,
700
- user: {
701
- id: user.id,
702
- email: user.email,
703
- name: user.name ?? undefined,
704
- profilePictureId: user.profilePictureId ?? null
705
- },
706
- companyFeatureEnabled
688
+ refreshToken
707
689
  };
708
690
  }
709
- generateLoginResponseWithCompany(user, company, branch) {
710
- const payload = {
691
+ generateLoginResponse(user) {
692
+ const { accessToken, refreshToken } = this.generateTokens({
711
693
  id: user.id,
712
694
  email: user.email,
713
- type: 'access',
714
- companyId: company.id,
715
- branchId: branch?.id
716
- };
717
- const accessToken = this.jwtService.sign(payload, {
718
- secret: this.authConfig.getJwtSecret(),
719
- expiresIn: this.authConfig.getJwtExpiration()
695
+ type: 'access'
720
696
  });
721
- 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({
722
705
  id: user.id,
723
706
  email: user.email,
724
- type: 'refresh',
707
+ type: 'access',
725
708
  companyId: company.id,
726
709
  branchId: branch?.id
727
- };
728
- const refreshToken = this.jwtService.sign(refreshPayload, {
729
- secret: this.authConfig.getRefreshTokenSecret(),
730
- expiresIn: this.authConfig.getRefreshTokenExpiration()
731
710
  });
732
711
  return {
733
712
  accessToken,
734
713
  refreshToken,
735
- user: {
736
- id: user.id,
737
- email: user.email,
738
- name: user.name ?? undefined,
739
- profilePictureId: user.profilePictureId ?? null
740
- },
741
- company: {
742
- id: company.id,
743
- name: company.name,
744
- slug: company.slug,
745
- logoId: company.logoId ?? null
746
- },
747
- branch: branch ? {
748
- id: branch.id,
749
- name: branch.name,
750
- slug: branch.slug,
751
- logoId: branch.logoId ?? null
752
- } : undefined,
753
- companyFeatureEnabled: true
714
+ user: this.toUserInfo(user),
715
+ company: this.toCompanyInfo(company),
716
+ branch: branch ? this.toBranchInfo(branch) : undefined
754
717
  };
755
718
  }
756
719
  async generateCompanySelectionResponse(user, companies, branches) {
@@ -758,55 +721,49 @@ export class AuthenticationService {
758
721
  throw new BadRequestException('Session service not available');
759
722
  }
760
723
  const sessionId = await this.sessionService.createSession(user.id);
761
- // Build companies with their branches
762
724
  const companiesWithBranches = companies.map((company)=>({
763
- id: company.id,
764
- name: company.name,
765
- slug: company.slug,
725
+ ...this.toCompanyInfo(company),
766
726
  isActive: company.isActive,
767
- logoId: company.logoId ?? null,
768
727
  branches: branches.filter((b)=>b.companyId === company.id).map((b)=>({
769
- id: b.id,
770
- name: b.name,
771
- slug: b.slug,
772
- isActive: b.isActive,
773
- logoId: b.logoId ?? null
728
+ ...this.toBranchInfo(b),
729
+ isActive: b.isActive
774
730
  }))
775
731
  }));
776
732
  return {
777
733
  requiresSelection: true,
778
734
  sessionId,
779
735
  expiresAt: new Date(Date.now() + 5 * 60 * 1000),
780
- user: {
781
- id: user.id,
782
- email: user.email,
783
- name: user.name ?? undefined,
784
- profilePictureId: user.profilePictureId ?? null
785
- },
736
+ user: this.toUserInfo(user),
786
737
  companies: companiesWithBranches
787
738
  };
788
739
  }
740
+ generateRegistrationResponse(user) {
741
+ return {
742
+ ...this.generateLoginResponse(user),
743
+ requiresEmailVerification: false
744
+ };
745
+ }
746
+ generateRegistrationResponseWithCompany(user, company, branch) {
747
+ return {
748
+ ...this.generateLoginResponseWithCompany(user, company, branch),
749
+ requiresEmailVerification: false
750
+ };
751
+ }
789
752
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
790
- constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, injectedUserRepository, injectedPermissionRepository, injectedCompanyRepository, injectedBranchRepository){
753
+ constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService){
791
754
  _define_property(this, "jwtService", void 0);
792
755
  _define_property(this, "authConfig", void 0);
793
756
  _define_property(this, "cacheManager", void 0);
794
757
  _define_property(this, "dataSourceProvider", void 0);
795
758
  _define_property(this, "sessionService", void 0);
796
- _define_property(this, "injectedUserRepository", void 0);
797
- _define_property(this, "injectedPermissionRepository", void 0);
798
- _define_property(this, "injectedCompanyRepository", void 0);
799
- _define_property(this, "injectedBranchRepository", void 0);
759
+ _define_property(this, "authEmailService", void 0);
800
760
  _define_property(this, "logger", void 0);
801
761
  this.jwtService = jwtService;
802
762
  this.authConfig = authConfig;
803
763
  this.cacheManager = cacheManager;
804
764
  this.dataSourceProvider = dataSourceProvider;
805
765
  this.sessionService = sessionService;
806
- this.injectedUserRepository = injectedUserRepository;
807
- this.injectedPermissionRepository = injectedPermissionRepository;
808
- this.injectedCompanyRepository = injectedCompanyRepository;
809
- this.injectedBranchRepository = injectedBranchRepository;
766
+ this.authEmailService = authEmailService;
810
767
  this.logger = new Logger(AuthenticationService.name);
811
768
  }
812
769
  }
@@ -823,13 +780,7 @@ AuthenticationService = _ts_decorate([
823
780
  _ts_param(4, Optional()),
824
781
  _ts_param(4, Inject(CompanySelectionSessionService)),
825
782
  _ts_param(5, Optional()),
826
- _ts_param(5, InjectRepository(User)),
827
- _ts_param(6, Optional()),
828
- _ts_param(6, InjectRepository(UserCompanyPermission)),
829
- _ts_param(7, Optional()),
830
- _ts_param(7, InjectRepository(Company)),
831
- _ts_param(8, Optional()),
832
- _ts_param(8, InjectRepository(CompanyBranch)),
783
+ _ts_param(5, Inject(AuthEmailService)),
833
784
  _ts_metadata("design:type", Function),
834
785
  _ts_metadata("design:paramtypes", [
835
786
  typeof JwtService === "undefined" ? Object : JwtService,
@@ -837,9 +788,6 @@ AuthenticationService = _ts_decorate([
837
788
  typeof HybridCache === "undefined" ? Object : HybridCache,
838
789
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
839
790
  typeof CompanySelectionSessionService === "undefined" ? Object : CompanySelectionSessionService,
840
- typeof Repository === "undefined" ? Object : Repository,
841
- typeof Repository === "undefined" ? Object : Repository,
842
- typeof Repository === "undefined" ? Object : Repository,
843
- typeof Repository === "undefined" ? Object : Repository
791
+ typeof AuthEmailService === "undefined" ? Object : AuthEmailService
844
792
  ])
845
793
  ], AuthenticationService);