@flusys/nestjs-auth 3.0.0-rc → 3.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.
- package/README.md +610 -95
- package/cjs/controllers/authentication.controller.js +2 -173
- package/cjs/controllers/email-verification.controller.js +220 -0
- package/cjs/controllers/index.js +1 -0
- package/cjs/dtos/authentication.dto.js +12 -2
- package/cjs/entities/index.js +16 -17
- package/cjs/entities/pending-registration.entity.js +9 -0
- package/cjs/modules/auth.module.js +33 -24
- package/cjs/services/auth-config.service.js +11 -7
- package/cjs/services/auth-datasource.provider.js +15 -27
- package/cjs/services/auth-email.service.js +8 -33
- package/cjs/services/authentication.service.js +155 -110
- package/cjs/services/user.service.js +28 -83
- package/controllers/authentication.controller.d.ts +2 -10
- package/controllers/email-verification.controller.d.ts +20 -0
- package/controllers/index.d.ts +1 -0
- package/dtos/authentication.dto.d.ts +1 -0
- package/entities/index.d.ts +5 -10
- package/entities/pending-registration.entity.d.ts +1 -0
- package/fesm/controllers/authentication.controller.js +3 -174
- package/fesm/controllers/email-verification.controller.js +210 -0
- package/fesm/controllers/index.js +1 -0
- package/fesm/dtos/authentication.dto.js +32 -25
- package/fesm/entities/index.js +17 -9
- package/fesm/entities/pending-registration.entity.js +9 -0
- package/fesm/interfaces/user-enricher.interface.js +2 -24
- package/fesm/modules/auth.module.js +35 -26
- package/fesm/services/auth-config.service.js +11 -7
- package/fesm/services/auth-datasource.provider.js +15 -27
- package/fesm/services/auth-email.service.js +8 -33
- package/fesm/services/authentication.service.js +145 -100
- package/fesm/services/user.service.js +28 -83
- package/interfaces/user-enricher.interface.d.ts +3 -2
- package/modules/auth.module.d.ts +3 -1
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +1 -0
- package/services/auth-datasource.provider.d.ts +1 -0
- package/services/auth-email.service.d.ts +1 -0
- package/services/authentication.service.d.ts +7 -1
- 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
|
-
|
|
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
|
-
|
|
54
|
+
getJwtExpiration() {
|
|
51
55
|
return this.config.jwtExpiration ?? '1h';
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
getRefreshTokenSecret() {
|
|
54
58
|
if (this.config.refreshTokenSecret) {
|
|
55
59
|
return this.config.refreshTokenSecret;
|
|
56
60
|
}
|
|
57
|
-
if (
|
|
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
|
-
|
|
70
|
+
getRefreshTokenExpiration() {
|
|
67
71
|
return this.config.refreshTokenExpiration ?? '7d';
|
|
68
72
|
}
|
|
69
|
-
|
|
73
|
+
getRefreshTokenCookieName() {
|
|
70
74
|
return this.config.refreshTokenCookieName ?? 'fsn_refresh_token';
|
|
71
75
|
}
|
|
72
76
|
// Frontend Configuration
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|