@flusys/nestjs-auth 2.0.0 → 3.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 (43) hide show
  1. package/README.md +610 -95
  2. package/cjs/controllers/authentication.controller.js +2 -173
  3. package/cjs/controllers/email-verification.controller.js +220 -0
  4. package/cjs/controllers/index.js +1 -0
  5. package/cjs/dtos/authentication.dto.js +12 -2
  6. package/cjs/entities/index.js +16 -17
  7. package/cjs/entities/pending-registration.entity.js +9 -0
  8. package/cjs/entities/user-company-permission.entity.js +3 -3
  9. package/cjs/modules/auth.module.js +33 -24
  10. package/cjs/services/auth-config.service.js +11 -7
  11. package/cjs/services/auth-datasource.provider.js +15 -27
  12. package/cjs/services/auth-email.service.js +8 -33
  13. package/cjs/services/authentication.service.js +123 -99
  14. package/cjs/services/user.service.js +28 -83
  15. package/controllers/authentication.controller.d.ts +2 -10
  16. package/controllers/email-verification.controller.d.ts +20 -0
  17. package/controllers/index.d.ts +1 -0
  18. package/dtos/authentication.dto.d.ts +1 -0
  19. package/entities/index.d.ts +5 -10
  20. package/entities/pending-registration.entity.d.ts +1 -0
  21. package/entities/user-company-permission.entity.d.ts +1 -1
  22. package/fesm/controllers/authentication.controller.js +3 -174
  23. package/fesm/controllers/email-verification.controller.js +210 -0
  24. package/fesm/controllers/index.js +1 -0
  25. package/fesm/dtos/authentication.dto.js +32 -25
  26. package/fesm/entities/index.js +17 -9
  27. package/fesm/entities/pending-registration.entity.js +9 -0
  28. package/fesm/entities/user-company-permission.entity.js +3 -3
  29. package/fesm/interfaces/user-enricher.interface.js +2 -24
  30. package/fesm/modules/auth.module.js +35 -26
  31. package/fesm/services/auth-config.service.js +11 -7
  32. package/fesm/services/auth-datasource.provider.js +15 -27
  33. package/fesm/services/auth-email.service.js +8 -33
  34. package/fesm/services/authentication.service.js +124 -100
  35. package/fesm/services/user.service.js +28 -83
  36. package/interfaces/user-enricher.interface.d.ts +3 -2
  37. package/modules/auth.module.d.ts +3 -1
  38. package/package.json +3 -3
  39. package/services/auth-config.service.d.ts +1 -0
  40. package/services/auth-datasource.provider.d.ts +1 -0
  41. package/services/auth-email.service.d.ts +1 -0
  42. package/services/authentication.service.d.ts +4 -1
  43. package/services/user.service.d.ts +5 -0
@@ -25,6 +25,7 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
+ import { envConfig } from '@flusys/nestjs-core/config';
28
29
  import { Inject, Injectable, InternalServerErrorException, Logger, Optional } from '@nestjs/common';
29
30
  import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
30
31
  import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
@@ -40,21 +41,24 @@ export class AuthConfigService {
40
41
  isCompanyFeatureEnabled() {
41
42
  return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
42
43
  }
44
+ isEmailVerificationEnabled() {
45
+ return this.options.bootstrapAppConfig?.enableEmailVerification ?? true;
46
+ }
43
47
  // JWT Configuration
44
- /** Get JWT secret key */ getJwtSecret() {
48
+ getJwtSecret() {
45
49
  if (!this.config.jwtSecret) {
46
50
  throw new InternalServerErrorException('[AuthModule] JWT_SECRET is required - set via environment or module config');
47
51
  }
48
52
  return this.config.jwtSecret;
49
53
  }
50
- /** Get JWT expiration time */ getJwtExpiration() {
54
+ getJwtExpiration() {
51
55
  return this.config.jwtExpiration ?? '1h';
52
56
  }
53
- /** Get refresh token secret key */ getRefreshTokenSecret() {
57
+ getRefreshTokenSecret() {
54
58
  if (this.config.refreshTokenSecret) {
55
59
  return this.config.refreshTokenSecret;
56
60
  }
57
- if (process.env.NODE_ENV === 'production') {
61
+ if (envConfig.isProduction()) {
58
62
  throw new InternalServerErrorException('[AuthModule] REFRESH_TOKEN_SECRET is required in production - ' + 'using the same secret for access and refresh tokens is a security risk');
59
63
  }
60
64
  if (!this.refreshTokenSecretWarned) {
@@ -63,14 +67,14 @@ export class AuthConfigService {
63
67
  }
64
68
  return this.getJwtSecret();
65
69
  }
66
- /** Get refresh token expiration time */ getRefreshTokenExpiration() {
70
+ getRefreshTokenExpiration() {
67
71
  return this.config.refreshTokenExpiration ?? '7d';
68
72
  }
69
- /** Get refresh token cookie name */ getRefreshTokenCookieName() {
73
+ getRefreshTokenCookieName() {
70
74
  return this.config.refreshTokenCookieName ?? 'fsn_refresh_token';
71
75
  }
72
76
  // Frontend Configuration
73
- /** Get frontend URL for email links */ getFrontendUrl() {
77
+ getFrontendUrl() {
74
78
  return this.config.frontendUrl;
75
79
  }
76
80
  constructor(injectedOptions){
@@ -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());
@@ -39,9 +39,6 @@ export class AuthEmailService {
39
39
  isEmailEnabled() {
40
40
  return !!this.emailProvider;
41
41
  }
42
- // ============================================
43
- // Private Helpers
44
- // ============================================
45
42
  async getRepository(entity) {
46
43
  if (!this.dataSourceProvider) {
47
44
  throw new InternalServerErrorException(`${String(entity)} repository not available`);
@@ -51,7 +48,7 @@ export class AuthEmailService {
51
48
  generateToken() {
52
49
  return crypto.randomBytes(32).toString('hex');
53
50
  }
54
- /** Hash token for secure storage - prevents token theft if database is compromised */ hashToken(token) {
51
+ hashToken(token) {
55
52
  return crypto.createHash('sha256').update(token).digest('hex');
56
53
  }
57
54
  getErrorMessage(error) {
@@ -79,9 +76,6 @@ export class AuthEmailService {
79
76
  throw new BadRequestException(usedMessage);
80
77
  }
81
78
  }
82
- // ============================================
83
- // Pending Registration (New Users)
84
- // ============================================
85
79
  async hasPendingRegistration(email) {
86
80
  if (!this.isEmailEnabled()) return false;
87
81
  const pendingRepository = await this.getRepository(PendingRegistration);
@@ -121,7 +115,8 @@ export class AuthEmailService {
121
115
  passwordHash,
122
116
  token: tokenHash,
123
117
  expiresAt,
124
- companyData: data.companyData ?? null
118
+ companyData: data.companyData ?? null,
119
+ additionalFields: data.additionalFields ?? null
125
120
  });
126
121
  await pendingRepository.save(pending);
127
122
  // Send verification email with raw token (user receives this)
@@ -132,12 +127,7 @@ export class AuthEmailService {
132
127
  message: 'Please check your email to verify your registration.'
133
128
  };
134
129
  }
135
- // ============================================
136
- // Password Reset (Existing Users)
137
- // ============================================
138
- /**
139
- * Request password reset - sends reset email
140
- */ async forgotPassword(email, resetUrlBase) {
130
+ async forgotPassword(email, resetUrlBase) {
141
131
  this.assertEmailEnabled();
142
132
  const userRepository = await this.getRepository(User);
143
133
  const tokenRepository = await this.getRepository(UserToken);
@@ -183,9 +173,7 @@ export class AuthEmailService {
183
173
  message: 'If the email exists, a password reset link has been sent.'
184
174
  };
185
175
  }
186
- /**
187
- * Reset password using token
188
- */ async resetPassword(token, newPassword) {
176
+ async resetPassword(token, newPassword) {
189
177
  const userRepository = await this.getRepository(User);
190
178
  const tokenRepository = await this.getRepository(UserToken);
191
179
  // Hash the incoming token to compare with stored hash
@@ -220,13 +208,7 @@ export class AuthEmailService {
220
208
  message: 'Password has been reset successfully.'
221
209
  };
222
210
  }
223
- // ============================================
224
- // Email Verification
225
- // ============================================
226
- /**
227
- * Verify email using token
228
- * Handles both pending registration and existing user verification
229
- */ async verifyEmail(token) {
211
+ async verifyEmail(token) {
230
212
  const pendingRepository = await this.getRepository(PendingRegistration);
231
213
  const userRepository = await this.getRepository(User);
232
214
  const tokenRepository = await this.getRepository(UserToken);
@@ -296,9 +278,7 @@ export class AuthEmailService {
296
278
  message: 'Email has been verified successfully.'
297
279
  };
298
280
  }
299
- /**
300
- * Resend verification email - handles both pending registrations and existing users
301
- */ async resendVerificationEmail(email, verifyUrlBase) {
281
+ async resendVerificationEmail(email, verifyUrlBase) {
302
282
  this.assertEmailEnabled();
303
283
  const pendingRepository = await this.getRepository(PendingRegistration);
304
284
  const userRepository = await this.getRepository(User);
@@ -353,12 +333,7 @@ export class AuthEmailService {
353
333
  message: 'If the email requires verification, a link has been sent.'
354
334
  };
355
335
  }
356
- // ============================================
357
- // Cleanup
358
- // ============================================
359
- /**
360
- * Clean up expired tokens and pending registrations
361
- */ async cleanupExpired() {
336
+ async cleanupExpired() {
362
337
  const tokenRepository = await this.getRepository(UserToken);
363
338
  const pendingRepository = await this.getRepository(PendingRegistration);
364
339
  const now = new Date();