@flusys/nestjs-auth 1.0.1 → 1.1.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.
@@ -32,8 +32,7 @@ import { Request } from 'express';
32
32
  import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
33
33
  import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
34
34
  export class AuthDataSourceProvider extends MultiTenantDataSourceService {
35
- // Override Static Cache Access Methods
36
- /** Override to access Auth-specific static properties */ async getSingleDataSource() {
35
+ async getSingleDataSource() {
37
36
  if (!AuthDataSourceProvider.singleDataSource) {
38
37
  if (AuthDataSourceProvider.singleConnectionLock) {
39
38
  return AuthDataSourceProvider.singleConnectionLock;
@@ -57,18 +56,15 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
57
56
  }
58
57
  return AuthDataSourceProvider.singleDataSource;
59
58
  }
60
- /** Override to use Auth-specific static cache for tenant connections */ async getOrCreateTenantConnection(tenant) {
61
- // Return existing initialized connection from Auth-specific cache
59
+ async getOrCreateTenantConnection(tenant) {
62
60
  const existing = AuthDataSourceProvider.tenantConnections.get(tenant.id);
63
61
  if (existing?.isInitialized) {
64
62
  return existing;
65
63
  }
66
- // If another request is creating this tenant's connection, wait for it
67
64
  const pendingConnection = AuthDataSourceProvider.connectionLocks.get(tenant.id);
68
65
  if (pendingConnection) {
69
66
  return pendingConnection;
70
67
  }
71
- // Create connection with lock to prevent race conditions
72
68
  const config = this.buildTenantDatabaseConfig(tenant);
73
69
  const connectionPromise = this.createDataSourceFromConfig(config);
74
70
  AuthDataSourceProvider.connectionLocks.set(tenant.id, connectionPromise);
@@ -80,8 +76,7 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
80
76
  AuthDataSourceProvider.connectionLocks.delete(tenant.id);
81
77
  }
82
78
  }
83
- // Factory Methods
84
- /** Build parent options from AuthModuleOptions */ static buildParentOptions(options) {
79
+ static buildParentOptions(options) {
85
80
  return {
86
81
  bootstrapAppConfig: options.bootstrapAppConfig,
87
82
  defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
@@ -89,35 +84,28 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
89
84
  tenants: options.config?.tenants
90
85
  };
91
86
  }
92
- // Feature Flags
93
- /** Check if company feature is enabled (tenant-specific or global fallback) */ isCompanyFeatureEnabled(tenant) {
87
+ isCompanyFeatureEnabled(tenant) {
94
88
  const effectiveTenant = tenant ?? this.getCurrentTenant();
95
89
  return effectiveTenant?.enableCompanyFeature ?? this.authOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
96
90
  }
97
- // Entity Management
98
- /** Get auth entities based on company feature flag */ async getAuthEntities() {
99
- const { Company, CompanyBranch, User, UserCompanyPermission, UserToken, PendingRegistration } = await import('../entities');
100
- const coreEntities = [
101
- User,
102
- UserToken,
103
- PendingRegistration
104
- ];
105
- return this.isCompanyFeatureEnabled() ? [
106
- ...coreEntities,
107
- Company,
108
- CompanyBranch,
109
- UserCompanyPermission
110
- ] : coreEntities;
91
+ isEmailVerificationEnabled(tenant) {
92
+ const effectiveTenant = tenant ?? this.getCurrentTenant();
93
+ return effectiveTenant?.enableEmailVerification ?? this.authOptions.bootstrapAppConfig?.enableEmailVerification ?? true;
94
+ }
95
+ async getAuthEntities() {
96
+ const { getEntitiesByConfig } = await import('../entities');
97
+ return getEntitiesByConfig({
98
+ enableCompanyFeature: this.isCompanyFeatureEnabled(),
99
+ enableEmailVerification: this.isEmailVerificationEnabled()
100
+ });
111
101
  }
112
- // Overrides
113
- /** Override to dynamically set entities based on tenant config */ async createDataSourceFromConfig(config) {
102
+ async createDataSourceFromConfig(config) {
114
103
  return super.createDataSourceFromConfig(config, await this.getAuthEntities());
115
104
  }
116
105
  constructor(authOptions, request){
117
106
  super(AuthDataSourceProvider.buildParentOptions(authOptions), request), _define_property(this, "authOptions", void 0), _define_property(this, "logger", void 0), this.authOptions = authOptions, this.logger = new Logger(AuthDataSourceProvider.name);
118
107
  }
119
108
  }
120
- // Override parent's static properties to have Auth-specific cache (isolated from IAM/Storage)
121
109
  _define_property(AuthDataSourceProvider, "tenantConnections", new Map());
122
110
  _define_property(AuthDataSourceProvider, "singleDataSource", null);
123
111
  _define_property(AuthDataSourceProvider, "tenantsRegistry", new Map());
@@ -42,9 +42,14 @@ import { AuthEmailService } from './auth-email.service';
42
42
  import { CompanySelectionSessionService } from './company-selection-session.service';
43
43
  export class AuthenticationService {
44
44
  // Feature Flags
45
- /** Check if company feature is enabled (tenant-aware with global fallback) */ isCompanyFeatureEnabled() {
45
+ isCompanyFeatureEnabled() {
46
46
  return this.dataSourceProvider?.isCompanyFeatureEnabled() ?? this.authConfig.isCompanyFeatureEnabled();
47
47
  }
48
+ isEmailVerificationEnabled() {
49
+ const featureEnabled = this.dataSourceProvider?.isEmailVerificationEnabled() ?? this.authConfig.isEmailVerificationEnabled();
50
+ const providerAvailable = this.authEmailService?.isEmailEnabled() ?? false;
51
+ return featureEnabled && providerAvailable;
52
+ }
48
53
  // Repository Getters
49
54
  async getUserRepository() {
50
55
  if (!this.dataSourceProvider) {
@@ -67,7 +72,7 @@ export class AuthenticationService {
67
72
  // Login
68
73
  async login(email, password) {
69
74
  // Check for pending registration first (user registered but hasn't verified email yet)
70
- if (await this.authEmailService?.hasPendingRegistration(email)) {
75
+ if (this.isEmailVerificationEnabled() && await this.authEmailService.hasPendingRegistration(email)) {
71
76
  return {
72
77
  requiresEmailVerification: true,
73
78
  email,
@@ -78,7 +83,7 @@ export class AuthenticationService {
78
83
  if (!user.isActive) {
79
84
  throw new UnauthorizedException('User account is deactivated');
80
85
  }
81
- if (this.authEmailService?.isEmailEnabled() && !user.emailVerified) {
86
+ if (this.isEmailVerificationEnabled() && !user.emailVerified) {
82
87
  return {
83
88
  requiresEmailVerification: true,
84
89
  email: user.email,
@@ -90,7 +95,7 @@ export class AuthenticationService {
90
95
  }
91
96
  return this.handleCompanyBasedLogin(user);
92
97
  }
93
- /** Handle login when company feature is enabled */ async handleCompanyBasedLogin(user) {
98
+ async handleCompanyBasedLogin(user) {
94
99
  const permissionRepository = await this.getPermissionRepository();
95
100
  const branchRepository = await this.getBranchRepository();
96
101
  if (!permissionRepository || !branchRepository) {
@@ -141,7 +146,7 @@ export class AuthenticationService {
141
146
  return await this.generateCompanySelectionResponse(user, companies, branches);
142
147
  }
143
148
  // Registration
144
- /** Register a new user. If email enabled, creates pending registration. Otherwise creates user directly. */ async register(dto, verifyUrlBase) {
149
+ async register(dto, verifyUrlBase) {
145
150
  const userRepository = await this.getUserRepository();
146
151
  const existingUser = await userRepository.findOne({
147
152
  where: {
@@ -157,7 +162,7 @@ export class AuthenticationService {
157
162
  throw new BadRequestException('Registration failed due to email exists. Please try again or contact support.');
158
163
  }
159
164
  // If email verification is enabled, create pending registration instead of user
160
- if (this.authEmailService?.isEmailEnabled() && verifyUrlBase) {
165
+ if (this.isEmailVerificationEnabled() && verifyUrlBase) {
161
166
  const companyData = this.isCompanyFeatureEnabled() ? {
162
167
  companySlug: dto.companySlug,
163
168
  branchSlug: dto.branchSlug,
@@ -181,7 +186,7 @@ export class AuthenticationService {
181
186
  // No email verification - create user directly
182
187
  return this.createUserDirectly(dto);
183
188
  }
184
- /** Handle registration for existing user when company feature is enabled */ async handleExistingUserCompanyRegistration(existingUser, dto) {
189
+ async handleExistingUserCompanyRegistration(existingUser, dto) {
185
190
  if (!dto.companySlug && !dto.newCompanyName) {
186
191
  throw new BadRequestException('Email already registered. Please login or provide a company to join/create.');
187
192
  }
@@ -213,7 +218,7 @@ export class AuthenticationService {
213
218
  }
214
219
  return this.processCompanyRegistration(existingUser, dto, true);
215
220
  }
216
- /** Create user directly (no email verification) */ async createUserDirectly(dto) {
221
+ async createUserDirectly(dto) {
217
222
  const userRepository = await this.getUserRepository();
218
223
  const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
219
224
  const user = userRepository.create({
@@ -231,7 +236,7 @@ export class AuthenticationService {
231
236
  }
232
237
  return this.processCompanyRegistration(user, dto);
233
238
  }
234
- /** Create user from pending registration (after email verification) */ async createUserFromPendingRegistration(pending) {
239
+ async createUserFromPendingRegistration(pending) {
235
240
  const userRepository = await this.getUserRepository();
236
241
  const existingUser = await userRepository.findOne({
237
242
  where: {
@@ -256,7 +261,7 @@ export class AuthenticationService {
256
261
  }
257
262
  return this.generateRegistrationResponse(user);
258
263
  }
259
- /** Process company registration (join existing or create new) */ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
264
+ async processCompanyRegistration(user, data, throwOnUnavailable = false) {
260
265
  const companyRepository = await this.getCompanyRepository();
261
266
  const branchRepository = await this.getBranchRepository();
262
267
  const permissionRepository = await this.getPermissionRepository();
@@ -271,7 +276,7 @@ export class AuthenticationService {
271
276
  }
272
277
  return this.generateRegistrationResponse(user);
273
278
  }
274
- /** Find existing company/branch or create new ones */ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
279
+ async findOrCreateCompanyAndBranch(data, companyRepo, branchRepo) {
275
280
  if (data.companySlug) {
276
281
  const company = await companyRepo.findOne({
277
282
  where: {
@@ -335,7 +340,7 @@ export class AuthenticationService {
335
340
  branch: null
336
341
  };
337
342
  }
338
- /** Assign user to company and optionally branch */ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
343
+ async assignUserPermissions(userId, companyId, branchId, permissionRepo) {
339
344
  await permissionRepo.save(permissionRepo.create({
340
345
  userId,
341
346
  permissionType: PermissionType.COMPANY,
@@ -350,7 +355,7 @@ export class AuthenticationService {
350
355
  }
351
356
  }
352
357
  // Company/Branch Selection
353
- /** Select company and branch (step 2 of login). Frontend receives all options and user selects both. */ async select(sessionId, companyId, branchId) {
358
+ async select(sessionId, companyId, branchId) {
354
359
  this.ensureCompanyFeatureEnabled();
355
360
  if (!this.sessionService) {
356
361
  throw new BadRequestException('Session service not available');
@@ -361,11 +366,11 @@ export class AuthenticationService {
361
366
  }
362
367
  return this.validateAndLoginWithCompany(session.userId, companyId, branchId);
363
368
  }
364
- /** Switch company/branch after login */ async switchCompany(userId, companyId, branchId) {
369
+ async switchCompany(userId, companyId, branchId) {
365
370
  this.ensureCompanyFeatureEnabled();
366
371
  return this.validateAndLoginWithCompany(userId, companyId, branchId);
367
372
  }
368
- /** Shared: Validate user access to company/branch and generate login response */ async validateAndLoginWithCompany(userId, companyId, branchId) {
373
+ async validateAndLoginWithCompany(userId, companyId, branchId) {
369
374
  const userRepository = await this.getUserRepository();
370
375
  const user = await userRepository.findOne({
371
376
  where: {
@@ -425,7 +430,7 @@ export class AuthenticationService {
425
430
  return this.generateLoginResponseWithCompany(user, company, branch);
426
431
  }
427
432
  // Token Management
428
- /** Refresh access token (with token rotation). Returns new access token and new refresh token. */ async refreshToken(refreshToken) {
433
+ async refreshToken(refreshToken) {
429
434
  try {
430
435
  const payload = this.jwtService.verify(refreshToken, {
431
436
  secret: this.authConfig.getRefreshTokenSecret()
@@ -459,7 +464,7 @@ export class AuthenticationService {
459
464
  throw new UnauthorizedException('Invalid or expired refresh token');
460
465
  }
461
466
  }
462
- /** Change password */ async changePassword(userId, currentPassword, newPassword) {
467
+ async changePassword(userId, currentPassword, newPassword) {
463
468
  const userRepository = await this.getUserRepository();
464
469
  const user = await userRepository.findOne({
465
470
  where: {
@@ -473,7 +478,7 @@ export class AuthenticationService {
473
478
  user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
474
479
  await userRepository.save(user);
475
480
  }
476
- /** Logout - Revokes all tokens. Stores revocation timestamp in cache. */ async logout(userId) {
481
+ async logout(userId) {
477
482
  if (!this.cacheManager) {
478
483
  // Cache is required for secure logout - tokens cannot be revoked without it
479
484
  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.');
@@ -487,7 +492,7 @@ export class AuthenticationService {
487
492
  await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
488
493
  this.logger.log(`User ${userId} logged out - all tokens revoked`);
489
494
  }
490
- /** Check if a token is revoked (issued before user's revocation timestamp) */ async isTokenRevoked(userId, tokenIssuedAt) {
495
+ async isTokenRevoked(userId, tokenIssuedAt) {
491
496
  if (!this.cacheManager || !tokenIssuedAt) {
492
497
  return false;
493
498
  }
@@ -499,7 +504,7 @@ export class AuthenticationService {
499
504
  // Token is revoked if it was issued before the revocation timestamp
500
505
  return tokenIssuedAt < revokedAt;
501
506
  }
502
- /** Parse JWT expiry string to milliseconds */ parseExpiryToMs(expiry) {
507
+ parseExpiryToMs(expiry) {
503
508
  const match = expiry.match(/^(\d+)([smhd])$/);
504
509
  if (!match) return 7 * 24 * 60 * 60 * 1000; // Default 7 days
505
510
  const value = parseInt(match[1], 10);
@@ -518,7 +523,7 @@ export class AuthenticationService {
518
523
  }
519
524
  }
520
525
  // Query Methods
521
- /** Get user's companies */ async getUserCompanies(userId) {
526
+ async getUserCompanies(userId) {
522
527
  this.ensureCompanyFeatureEnabled();
523
528
  const permissionRepository = await this.getPermissionRepository();
524
529
  const companyRepository = await this.getCompanyRepository();
@@ -539,7 +544,7 @@ export class AuthenticationService {
539
544
  active: true
540
545
  }).orderBy('c.name', 'ASC').getMany();
541
546
  }
542
- /** Get branches for a company that user has access to */ async getUserBranches(userId, companyId) {
547
+ async getUserBranches(userId, companyId) {
543
548
  this.ensureCompanyFeatureEnabled();
544
549
  const permissionRepository = await this.getPermissionRepository();
545
550
  const branchRepository = await this.getBranchRepository();
@@ -574,7 +579,7 @@ export class AuthenticationService {
574
579
  active: true
575
580
  }).orderBy('b.serial', 'ASC').addOrderBy('b.name', 'ASC').getMany();
576
581
  }
577
- /** Get company by ID */ async getCompanyById(companyId) {
582
+ async getCompanyById(companyId) {
578
583
  const companyRepository = await this.getCompanyRepository();
579
584
  if (!companyRepository) {
580
585
  return null;
@@ -585,7 +590,7 @@ export class AuthenticationService {
585
590
  }
586
591
  });
587
592
  }
588
- /** Get branch by ID */ async getBranchById(branchId) {
593
+ async getBranchById(branchId) {
589
594
  const branchRepository = await this.getBranchRepository();
590
595
  if (!branchRepository) {
591
596
  return null;
@@ -596,7 +601,7 @@ export class AuthenticationService {
596
601
  }
597
602
  });
598
603
  }
599
- /** Get user info for /auth/me endpoint. Returns user, company, and branch info (no tokens). */ async getUserInfo(userId, companyId, branchId) {
604
+ async getUserInfo(userId, companyId, branchId) {
600
605
  const userRepository = await this.getUserRepository();
601
606
  const user = await userRepository.findOne({
602
607
  where: {
@@ -643,7 +648,7 @@ export class AuthenticationService {
643
648
  return user;
644
649
  }
645
650
  // Response Generators
646
- /** Create user info object */ toUserInfo(user) {
651
+ toUserInfo(user) {
647
652
  return {
648
653
  id: user.id,
649
654
  email: user.email,
@@ -652,7 +657,7 @@ export class AuthenticationService {
652
657
  profilePictureId: user.profilePictureId ?? null
653
658
  };
654
659
  }
655
- /** Create company info object */ toCompanyInfo(company) {
660
+ toCompanyInfo(company) {
656
661
  return {
657
662
  id: company.id,
658
663
  name: company.name,
@@ -660,7 +665,7 @@ export class AuthenticationService {
660
665
  logoId: company.logoId ?? null
661
666
  };
662
667
  }
663
- /** Create branch info object */ toBranchInfo(branch) {
668
+ toBranchInfo(branch) {
664
669
  return {
665
670
  id: branch.id,
666
671
  name: branch.name,
@@ -668,7 +673,7 @@ export class AuthenticationService {
668
673
  logoId: branch.logoId ?? null
669
674
  };
670
675
  }
671
- /** Generate JWT tokens */ generateTokens(payload) {
676
+ generateTokens(payload) {
672
677
  const accessToken = this.jwtService.sign({
673
678
  ...payload,
674
679
  type: 'access'
@@ -1,13 +1,15 @@
1
+ import { IBootstrapAppConfig } from '@flusys/nestjs-core';
1
2
  import { DynamicModule } from '@nestjs/common';
2
3
  import { AuthModuleAsyncOptions, AuthModuleOptions } from '../interfaces/auth-module-options.interface';
3
4
  export declare class AuthModule {
4
5
  static forRoot(options: AuthModuleOptions): DynamicModule;
5
6
  static forRootAsync(options: AuthModuleAsyncOptions): DynamicModule;
6
7
  private static buildModule;
8
+ private static getFeatureFlags;
7
9
  private static buildImports;
8
10
  private static buildControllers;
9
11
  private static buildProviders;
10
12
  private static buildExports;
11
13
  private static createAsyncProviders;
12
- static getEntities(enableCompanyFeature?: boolean): any[];
14
+ static getEntities(config: IBootstrapAppConfig): any[];
13
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-auth",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Authentication and authorization infrastructure for NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -91,7 +91,7 @@
91
91
  "typeorm": "^0.3.0"
92
92
  },
93
93
  "dependencies": {
94
- "@flusys/nestjs-core": "1.0.1",
95
- "@flusys/nestjs-shared": "1.0.1"
94
+ "@flusys/nestjs-core": "1.1.0",
95
+ "@flusys/nestjs-shared": "1.1.0"
96
96
  }
97
97
  }
@@ -10,6 +10,7 @@ export declare class AuthConfigService implements IModuleConfigService {
10
10
  getDatabaseMode(): DatabaseMode;
11
11
  isMultiTenant(): boolean;
12
12
  isCompanyFeatureEnabled(): boolean;
13
+ isEmailVerificationEnabled(): boolean;
13
14
  getJwtSecret(): string;
14
15
  getJwtExpiration(): string;
15
16
  getRefreshTokenSecret(): string;
@@ -18,6 +18,7 @@ export declare class AuthDataSourceProvider extends MultiTenantDataSourceService
18
18
  protected getOrCreateTenantConnection(tenant: ITenantDatabaseConfig): Promise<DataSource>;
19
19
  private static buildParentOptions;
20
20
  isCompanyFeatureEnabled(tenant?: ITenantDatabaseConfig): boolean;
21
+ isEmailVerificationEnabled(tenant?: ITenantDatabaseConfig): boolean;
21
22
  getAuthEntities(): Promise<any[]>;
22
23
  protected createDataSourceFromConfig(config: IDatabaseConfig): Promise<DataSource>;
23
24
  }
@@ -19,6 +19,7 @@ export declare class AuthenticationService {
19
19
  private readonly logger;
20
20
  constructor(jwtService: JwtService, authConfig: AuthConfigService, cacheManager?: HybridCache, dataSourceProvider?: AuthDataSourceProvider, sessionService?: CompanySelectionSessionService, authEmailService?: AuthEmailService);
21
21
  private isCompanyFeatureEnabled;
22
+ private isEmailVerificationEnabled;
22
23
  private getUserRepository;
23
24
  private getPermissionRepository;
24
25
  private getCompanyRepository;