@lenne.tech/nest-server 11.7.0 → 11.7.2
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/dist/config.env.js +17 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +35 -15
- package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.controller.js +29 -3
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +14 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.resolver.js +21 -3
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
- package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
- package/dist/core/modules/auth/services/core-auth.service.js +141 -9
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +395 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +29 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.module.js +46 -18
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
- package/dist/core/modules/better-auth/better-auth.service.js +209 -8
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +15 -2
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +7 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +72 -12
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +7 -1
- package/dist/core/modules/user/core-user.service.js +57 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
- package/dist/core.module.d.ts +3 -0
- package/dist/core.module.js +136 -55
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/auth/auth.resolver.js +2 -0
- package/dist/server/modules/auth/auth.resolver.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +2 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +5 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +27 -11
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/user/user.controller.js +0 -8
- package/dist/server/modules/user/user.controller.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +3 -1
- package/dist/server/modules/user/user.service.js +7 -3
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +32 -2
- package/src/core/common/interfaces/server-options.interface.ts +304 -58
- package/src/core/modules/auth/core-auth.controller.ts +94 -6
- package/src/core/modules/auth/core-auth.module.ts +15 -1
- package/src/core/modules/auth/core-auth.resolver.ts +71 -3
- package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
- package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
- package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
- package/src/core/modules/auth/services/core-auth.service.ts +245 -6
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +255 -0
- package/src/core/modules/better-auth/README.md +565 -208
- package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +737 -0
- package/src/core/modules/better-auth/better-auth.config.ts +45 -15
- package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
- package/src/core/modules/better-auth/better-auth.module.ts +83 -27
- package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
- package/src/core/modules/better-auth/better-auth.service.ts +367 -12
- package/src/core/modules/better-auth/better-auth.types.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -3
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +136 -16
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/user/core-user.service.ts +131 -4
- package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
- package/src/core.module.ts +264 -76
- package/src/index.ts +5 -0
- package/src/server/modules/auth/auth.resolver.ts +8 -0
- package/src/server/modules/better-auth/better-auth.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.resolver.ts +18 -11
- package/src/server/modules/user/user.controller.ts +1 -9
- package/src/server/modules/user/user.service.ts +4 -2
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
Logger,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
Optional,
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
} from '@nestjs/common';
|
|
2
9
|
import { JwtService } from '@nestjs/jwt';
|
|
3
10
|
import bcrypt = require('bcrypt');
|
|
4
11
|
import { randomUUID } from 'crypto';
|
|
@@ -8,6 +15,8 @@ import { getStringIds } from '../../../common/helpers/db.helper';
|
|
|
8
15
|
import { prepareServiceOptions } from '../../../common/helpers/service.helper';
|
|
9
16
|
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
10
17
|
import { ConfigService } from '../../../common/services/config.service';
|
|
18
|
+
import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
|
|
19
|
+
import { BetterAuthService } from '../../better-auth/better-auth.service';
|
|
11
20
|
import { CoreAuthModel } from '../core-auth.model';
|
|
12
21
|
import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
|
|
13
22
|
import { CoreAuthSignUpInput } from '../inputs/core-auth-sign-up.input';
|
|
@@ -29,9 +38,23 @@ export interface GetResultOptions {
|
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* CoreAuthService to handle user authentication
|
|
41
|
+
*
|
|
42
|
+
* When Better-Auth (IAM) is enabled, this service delegates authentication to IAM
|
|
43
|
+
* while maintaining backwards compatibility by returning Legacy JWT format tokens.
|
|
44
|
+
*
|
|
45
|
+
* Migration strategy:
|
|
46
|
+
* - New users: Created directly in IAM with scrypt password hash
|
|
47
|
+
* - Existing Legacy users: Lazily migrated on first sign-in
|
|
48
|
+
* (Legacy bcrypt password verified, then IAM account created with scrypt hash)
|
|
49
|
+
*
|
|
50
|
+
* @deprecated The signIn and signUp methods are deprecated when IAM is enabled.
|
|
51
|
+
* Use the IAM REST endpoints (/iam/sign-in/email, /iam/sign-up/email) directly
|
|
52
|
+
* for new implementations. Legacy endpoints remain for backwards compatibility.
|
|
32
53
|
*/
|
|
33
54
|
@Injectable()
|
|
34
55
|
export class CoreAuthService {
|
|
56
|
+
private readonly logger = new Logger(CoreAuthService.name);
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Integrate services
|
|
37
60
|
*/
|
|
@@ -39,6 +62,8 @@ export class CoreAuthService {
|
|
|
39
62
|
protected readonly userService: CoreAuthUserService,
|
|
40
63
|
protected readonly jwtService: JwtService,
|
|
41
64
|
protected readonly configService: ConfigService,
|
|
65
|
+
@Optional() protected readonly betterAuthService?: BetterAuthService,
|
|
66
|
+
@Optional() protected readonly betterAuthUserMapper?: BetterAuthUserMapper,
|
|
42
67
|
) {}
|
|
43
68
|
|
|
44
69
|
/**
|
|
@@ -99,6 +124,14 @@ export class CoreAuthService {
|
|
|
99
124
|
|
|
100
125
|
/**
|
|
101
126
|
* User sign in via email
|
|
127
|
+
*
|
|
128
|
+
* When IAM is enabled, this method:
|
|
129
|
+
* 1. For migrated users (have iamId): Verifies password via IAM
|
|
130
|
+
* 2. For non-migrated users: Verifies via Legacy, then migrates to IAM
|
|
131
|
+
*
|
|
132
|
+
* Always returns Legacy JWT format for backwards compatibility.
|
|
133
|
+
*
|
|
134
|
+
* @deprecated When IAM is enabled, prefer using /iam/sign-in/email REST endpoint directly.
|
|
102
135
|
*/
|
|
103
136
|
async signIn(input: CoreAuthSignInInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
104
137
|
// Check input
|
|
@@ -106,6 +139,9 @@ export class CoreAuthService {
|
|
|
106
139
|
throw new BadRequestException('Missing input');
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
// Check if user enumeration prevention is enabled
|
|
143
|
+
const preventUserEnumeration = this.configService.getFastButReadOnly('auth.preventUserEnumeration', false);
|
|
144
|
+
|
|
109
145
|
// Prepare service options
|
|
110
146
|
const serviceOptionsForUserService = prepareServiceOptions(serviceOptions, {
|
|
111
147
|
// We need password, so we can't use prepare output handling and have to deactivate it
|
|
@@ -119,12 +155,47 @@ export class CoreAuthService {
|
|
|
119
155
|
const { deviceDescription, deviceId, email, password } = input;
|
|
120
156
|
|
|
121
157
|
// Get user
|
|
122
|
-
|
|
158
|
+
let user: ICoreAuthUser;
|
|
159
|
+
try {
|
|
160
|
+
user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof NotFoundException) {
|
|
163
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
123
167
|
if (!user) {
|
|
124
|
-
throw new UnauthorizedException('Unknown email');
|
|
168
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
|
|
125
169
|
}
|
|
126
|
-
|
|
127
|
-
|
|
170
|
+
|
|
171
|
+
// Determine if IAM delegation is available
|
|
172
|
+
const iamEnabled = this.isIamEnabled();
|
|
173
|
+
|
|
174
|
+
if (iamEnabled && user.iamId) {
|
|
175
|
+
// User is already migrated to IAM - verify via IAM
|
|
176
|
+
const iamVerified = await this.verifyPasswordViaIam(email, password);
|
|
177
|
+
if (!iamVerified) {
|
|
178
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
|
|
179
|
+
}
|
|
180
|
+
this.logger.debug(`User ${email} authenticated via IAM (already migrated)`);
|
|
181
|
+
} else {
|
|
182
|
+
// Verify via Legacy (bcrypt)
|
|
183
|
+
// Check if user has a password (social login only users don't have one)
|
|
184
|
+
if (!user.password) {
|
|
185
|
+
throw new UnauthorizedException(
|
|
186
|
+
preventUserEnumeration ? 'Invalid credentials' : 'No password set for this account',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
!((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))
|
|
191
|
+
) {
|
|
192
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If IAM is enabled but user not migrated, migrate them now
|
|
196
|
+
if (iamEnabled && !user.iamId) {
|
|
197
|
+
await this.migrateUserToIam(user, email, password);
|
|
198
|
+
}
|
|
128
199
|
}
|
|
129
200
|
|
|
130
201
|
// Return tokens and user with currentUser set so securityCheck knows user is requesting own data
|
|
@@ -136,6 +207,13 @@ export class CoreAuthService {
|
|
|
136
207
|
|
|
137
208
|
/**
|
|
138
209
|
* Register a new user account
|
|
210
|
+
*
|
|
211
|
+
* When IAM is enabled, this method:
|
|
212
|
+
* 1. Creates the user in IAM first (with scrypt password hash)
|
|
213
|
+
* 2. Creates/links the Legacy user with iamId
|
|
214
|
+
* 3. Returns Legacy JWT format for backwards compatibility
|
|
215
|
+
*
|
|
216
|
+
* @deprecated When IAM is enabled, prefer using /iam/sign-up/email REST endpoint directly.
|
|
139
217
|
*/
|
|
140
218
|
async signUp(input: CoreAuthSignUpInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
141
219
|
// Prepare service options
|
|
@@ -146,7 +224,19 @@ export class CoreAuthService {
|
|
|
146
224
|
|
|
147
225
|
// Get and check user
|
|
148
226
|
try {
|
|
149
|
-
|
|
227
|
+
// Determine if IAM delegation is available
|
|
228
|
+
const iamEnabled = this.isIamEnabled();
|
|
229
|
+
|
|
230
|
+
let user: ICoreAuthUser;
|
|
231
|
+
|
|
232
|
+
if (iamEnabled) {
|
|
233
|
+
// Create via IAM first, then create/link Legacy user
|
|
234
|
+
user = await this.createUserViaIam(input, serviceOptionsForUserService);
|
|
235
|
+
} else {
|
|
236
|
+
// Create via Legacy
|
|
237
|
+
user = await this.userService.create(input, serviceOptionsForUserService);
|
|
238
|
+
}
|
|
239
|
+
|
|
150
240
|
if (!user) {
|
|
151
241
|
throw new BadRequestException('Email address already in use');
|
|
152
242
|
}
|
|
@@ -332,4 +422,153 @@ export class CoreAuthService {
|
|
|
332
422
|
// Return new token
|
|
333
423
|
return newRefreshToken;
|
|
334
424
|
}
|
|
425
|
+
|
|
426
|
+
// ===================================================================================================================
|
|
427
|
+
// IAM Delegation Helper Methods
|
|
428
|
+
// ===================================================================================================================
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Checks if IAM (Better-Auth) delegation is available and enabled
|
|
432
|
+
*/
|
|
433
|
+
protected isIamEnabled(): boolean {
|
|
434
|
+
return !!(this.betterAuthService?.isEnabled() && this.betterAuthUserMapper);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Verifies password via IAM for already-migrated users
|
|
439
|
+
*
|
|
440
|
+
* @param email - User email
|
|
441
|
+
* @param password - Plain password to verify
|
|
442
|
+
* @returns true if password is valid, false otherwise
|
|
443
|
+
*/
|
|
444
|
+
protected async verifyPasswordViaIam(email: string, password: string): Promise<boolean> {
|
|
445
|
+
if (!this.betterAuthService) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const api = this.betterAuthService.getApi();
|
|
450
|
+
if (!api) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const response = await api.signInEmail({
|
|
456
|
+
body: { email, password },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Check if response indicates successful authentication
|
|
460
|
+
return !!(response && 'user' in response && response.user);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
this.logger.debug(
|
|
463
|
+
`IAM password verification failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
464
|
+
);
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Migrates a Legacy user to IAM
|
|
471
|
+
*
|
|
472
|
+
* This creates the IAM user and account with a scrypt password hash,
|
|
473
|
+
* then links the Legacy user via iamId.
|
|
474
|
+
*
|
|
475
|
+
* @param user - The Legacy user to migrate
|
|
476
|
+
* @param email - User email
|
|
477
|
+
* @param plainPassword - Plain password (needed to create scrypt hash)
|
|
478
|
+
*/
|
|
479
|
+
protected async migrateUserToIam(user: ICoreAuthUser, email: string, plainPassword: string): Promise<void> {
|
|
480
|
+
if (!this.betterAuthUserMapper) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// Create IAM account with the plain password (creates scrypt hash)
|
|
486
|
+
const migrated = await this.betterAuthUserMapper.migrateAccountToIam(email, plainPassword);
|
|
487
|
+
|
|
488
|
+
if (migrated) {
|
|
489
|
+
this.logger.log(`Migrated Legacy user ${email} to IAM`);
|
|
490
|
+
|
|
491
|
+
// Refresh user to get updated iamId
|
|
492
|
+
const updatedUser = await this.userService.getViaEmail(email, { force: true });
|
|
493
|
+
if (updatedUser?.iamId) {
|
|
494
|
+
// Update the user object in place so subsequent operations see the iamId
|
|
495
|
+
(user as any).iamId = updatedUser.iamId;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// Log but don't throw - migration failure shouldn't block login
|
|
500
|
+
this.logger.warn(
|
|
501
|
+
`Failed to migrate user ${email} to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Creates a user via IAM and links to Legacy user
|
|
508
|
+
*
|
|
509
|
+
* @param input - Sign-up input data
|
|
510
|
+
* @param serviceOptions - Service options for user service
|
|
511
|
+
* @returns The created Legacy user (linked to IAM)
|
|
512
|
+
*/
|
|
513
|
+
protected async createUserViaIam(input: CoreAuthSignUpInput, serviceOptions: ServiceOptions): Promise<ICoreAuthUser> {
|
|
514
|
+
if (!this.betterAuthService || !this.betterAuthUserMapper) {
|
|
515
|
+
throw new BadRequestException('IAM service not available');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const api = this.betterAuthService.getApi();
|
|
519
|
+
if (!api) {
|
|
520
|
+
throw new BadRequestException('IAM API not available');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
// Create user in IAM first
|
|
525
|
+
// Note: firstName and lastName are project-specific fields, may not exist on CoreAuthSignUpInput
|
|
526
|
+
const inputAny = input as any;
|
|
527
|
+
const name = [inputAny.firstName, inputAny.lastName].filter(Boolean).join(' ') || input.email.split('@')[0];
|
|
528
|
+
const response = await api.signUpEmail({
|
|
529
|
+
body: {
|
|
530
|
+
email: input.email,
|
|
531
|
+
name,
|
|
532
|
+
password: input.password,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
if (!response || !('user' in response) || !response.user) {
|
|
537
|
+
throw new BadRequestException('Email address already in use');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Link or create Legacy user with iamId
|
|
541
|
+
const iamUser = response.user as { email: string; id: string; name?: string };
|
|
542
|
+
const syncedUser = await this.betterAuthUserMapper.linkOrCreateUser(iamUser as any, {
|
|
543
|
+
firstName: inputAny.firstName,
|
|
544
|
+
lastName: inputAny.lastName,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (!syncedUser) {
|
|
548
|
+
throw new BadRequestException('Failed to create user');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Sync password to Legacy (enables backwards compatibility)
|
|
552
|
+
// Pass plain password so bcrypt hash can be created for Legacy Auth
|
|
553
|
+
await this.betterAuthUserMapper.syncPasswordToLegacy(iamUser.id, input.email, input.password);
|
|
554
|
+
|
|
555
|
+
this.logger.log(`Created user ${input.email} via IAM`);
|
|
556
|
+
|
|
557
|
+
// Get the full user from our database
|
|
558
|
+
const user = await this.userService.getViaEmail(input.email, serviceOptions);
|
|
559
|
+
if (!user) {
|
|
560
|
+
throw new BadRequestException('Failed to retrieve created user');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return user;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
566
|
+
this.logger.debug(`IAM sign-up error for ${input.email}: ${errorMessage}`);
|
|
567
|
+
|
|
568
|
+
if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
|
|
569
|
+
throw new BadRequestException('Email address already in use');
|
|
570
|
+
}
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
335
574
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { IAuthRateLimit } from '../../../common/interfaces/server-options.interface';
|
|
4
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rate limit entry for tracking requests
|
|
8
|
+
*/
|
|
9
|
+
interface RateLimitEntry {
|
|
10
|
+
count: number;
|
|
11
|
+
resetTime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result of a rate limit check
|
|
16
|
+
*
|
|
17
|
+
* @internal This interface is identical to BetterAuthRateLimiter's RateLimitResult.
|
|
18
|
+
* Use the exported RateLimitResult from better-auth module if needed externally.
|
|
19
|
+
*/
|
|
20
|
+
interface RateLimitResult {
|
|
21
|
+
/** Whether the request is allowed */
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
/** Current request count in the window */
|
|
24
|
+
current: number;
|
|
25
|
+
/** Maximum requests allowed */
|
|
26
|
+
limit: number;
|
|
27
|
+
/** Number of remaining requests in the window */
|
|
28
|
+
remaining: number;
|
|
29
|
+
/** Seconds until the rate limit resets */
|
|
30
|
+
resetIn: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default rate limiting configuration
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_CONFIG: Required<IAuthRateLimit> = {
|
|
37
|
+
enabled: false,
|
|
38
|
+
max: 10,
|
|
39
|
+
message: 'Too many requests, please try again later.',
|
|
40
|
+
windowSeconds: 60,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* In-memory rate limiter for Legacy Auth endpoints
|
|
45
|
+
*
|
|
46
|
+
* This service provides rate limiting to protect against brute-force attacks
|
|
47
|
+
* on authentication endpoints. It uses an in-memory store with automatic cleanup.
|
|
48
|
+
*
|
|
49
|
+
* Features:
|
|
50
|
+
* - Configurable request limits and time windows
|
|
51
|
+
* - Automatic cleanup of expired entries
|
|
52
|
+
* - IP-based tracking
|
|
53
|
+
* - Auto-configuration from ConfigService
|
|
54
|
+
*
|
|
55
|
+
* Configuration via config.env.ts:
|
|
56
|
+
* ```typescript
|
|
57
|
+
* auth: {
|
|
58
|
+
* rateLimit: {
|
|
59
|
+
* enabled: true,
|
|
60
|
+
* max: 10,
|
|
61
|
+
* windowSeconds: 60,
|
|
62
|
+
* message: 'Too many login attempts, please try again later.',
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @since 11.7.x
|
|
68
|
+
*/
|
|
69
|
+
@Injectable()
|
|
70
|
+
export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
71
|
+
private readonly logger = new Logger(LegacyAuthRateLimiter.name);
|
|
72
|
+
private readonly store = new Map<string, RateLimitEntry>();
|
|
73
|
+
private config: Required<IAuthRateLimit> = DEFAULT_CONFIG;
|
|
74
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
// Start cleanup interval (every 5 minutes)
|
|
78
|
+
this.startCleanup();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Auto-configure from ConfigService on module initialization
|
|
83
|
+
*/
|
|
84
|
+
onModuleInit(): void {
|
|
85
|
+
const rateLimitConfig = ConfigService.getFastButReadOnly<IAuthRateLimit>('auth.rateLimit');
|
|
86
|
+
if (rateLimitConfig) {
|
|
87
|
+
this.configure(rateLimitConfig);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Configure the rate limiter
|
|
93
|
+
*
|
|
94
|
+
* Follows the "presence implies enabled" pattern:
|
|
95
|
+
* - If config is undefined/null: rate limiting is disabled (backward compatible)
|
|
96
|
+
* - If config is an object (even empty {}): rate limiting is enabled by default
|
|
97
|
+
* - Unless `enabled: false` is explicitly set to disable while pre-configuring
|
|
98
|
+
*
|
|
99
|
+
* @param config - Rate limiting configuration (presence implies enabled)
|
|
100
|
+
*/
|
|
101
|
+
configure(config: IAuthRateLimit | null | undefined): void {
|
|
102
|
+
// If config is not provided, rate limiting stays disabled (backward compatible)
|
|
103
|
+
if (config === undefined || config === null) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Presence of config implies enabled, unless explicitly disabled
|
|
108
|
+
const enabled = config.enabled !== false;
|
|
109
|
+
|
|
110
|
+
this.config = {
|
|
111
|
+
...DEFAULT_CONFIG,
|
|
112
|
+
...config,
|
|
113
|
+
enabled,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (this.config.enabled) {
|
|
117
|
+
this.logger.debug(
|
|
118
|
+
`Legacy Auth rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a request is allowed under the rate limit
|
|
125
|
+
*
|
|
126
|
+
* @param ip - Client IP address
|
|
127
|
+
* @param endpoint - Endpoint name (e.g., 'signIn', 'signUp')
|
|
128
|
+
* @returns Rate limit check result
|
|
129
|
+
*/
|
|
130
|
+
check(ip: string, endpoint: string): RateLimitResult {
|
|
131
|
+
// If rate limiting is disabled, always allow
|
|
132
|
+
if (!this.config.enabled) {
|
|
133
|
+
return {
|
|
134
|
+
allowed: true,
|
|
135
|
+
current: 0,
|
|
136
|
+
limit: Infinity,
|
|
137
|
+
remaining: Infinity,
|
|
138
|
+
resetIn: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const limit = this.config.max;
|
|
143
|
+
const key = `${ip}:${endpoint}`;
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
|
|
146
|
+
// Get or create entry
|
|
147
|
+
let entry = this.store.get(key);
|
|
148
|
+
|
|
149
|
+
if (!entry || now >= entry.resetTime) {
|
|
150
|
+
// Create new entry or reset expired one
|
|
151
|
+
entry = {
|
|
152
|
+
count: 1,
|
|
153
|
+
resetTime: now + this.config.windowSeconds * 1000,
|
|
154
|
+
};
|
|
155
|
+
this.store.set(key, entry);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
allowed: true,
|
|
159
|
+
current: 1,
|
|
160
|
+
limit,
|
|
161
|
+
remaining: limit - 1,
|
|
162
|
+
resetIn: this.config.windowSeconds,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Increment count
|
|
167
|
+
entry.count++;
|
|
168
|
+
|
|
169
|
+
const resetIn = Math.ceil((entry.resetTime - now) / 1000);
|
|
170
|
+
const allowed = entry.count <= limit;
|
|
171
|
+
const remaining = Math.max(0, limit - entry.count);
|
|
172
|
+
|
|
173
|
+
if (!allowed) {
|
|
174
|
+
this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${endpoint}: ${entry.count}/${limit}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
allowed,
|
|
179
|
+
current: entry.count,
|
|
180
|
+
limit,
|
|
181
|
+
remaining,
|
|
182
|
+
resetIn,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the configured error message
|
|
188
|
+
*/
|
|
189
|
+
getMessage(): string {
|
|
190
|
+
return this.config.message;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if rate limiting is enabled
|
|
195
|
+
*/
|
|
196
|
+
isEnabled(): boolean {
|
|
197
|
+
return this.config.enabled;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Reset rate limit for a specific IP (useful for testing or admin override)
|
|
202
|
+
*
|
|
203
|
+
* @param ip - Client IP address
|
|
204
|
+
*/
|
|
205
|
+
reset(ip: string): void {
|
|
206
|
+
for (const key of this.store.keys()) {
|
|
207
|
+
if (key.startsWith(`${ip}:`)) {
|
|
208
|
+
this.store.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clear all rate limit entries (useful for testing)
|
|
215
|
+
*/
|
|
216
|
+
clear(): void {
|
|
217
|
+
this.store.clear();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get statistics about the rate limiter
|
|
222
|
+
*/
|
|
223
|
+
getStats(): { activeEntries: number; enabled: boolean } {
|
|
224
|
+
return {
|
|
225
|
+
activeEntries: this.store.size,
|
|
226
|
+
enabled: this.config.enabled,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Stop the cleanup interval (for graceful shutdown)
|
|
232
|
+
*/
|
|
233
|
+
onModuleDestroy(): void {
|
|
234
|
+
if (this.cleanupInterval) {
|
|
235
|
+
clearInterval(this.cleanupInterval);
|
|
236
|
+
this.cleanupInterval = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Mask IP address for logging (privacy)
|
|
242
|
+
*/
|
|
243
|
+
private maskIp(ip: string): string {
|
|
244
|
+
if (ip.includes('.')) {
|
|
245
|
+
// IPv4: show first two octets
|
|
246
|
+
const parts = ip.split('.');
|
|
247
|
+
return `${parts[0]}.${parts[1]}.*.*`;
|
|
248
|
+
}
|
|
249
|
+
// IPv6: show first segment
|
|
250
|
+
const parts = ip.split(':');
|
|
251
|
+
return `${parts[0]}:****`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Start periodic cleanup of expired entries
|
|
256
|
+
*/
|
|
257
|
+
private startCleanup(): void {
|
|
258
|
+
// Clean up every 5 minutes
|
|
259
|
+
this.cleanupInterval = setInterval(
|
|
260
|
+
() => {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
let cleaned = 0;
|
|
263
|
+
|
|
264
|
+
for (const [key, entry] of this.store.entries()) {
|
|
265
|
+
if (now >= entry.resetTime) {
|
|
266
|
+
this.store.delete(key);
|
|
267
|
+
cleaned++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cleaned > 0) {
|
|
272
|
+
this.logger.debug(`Cleaned up ${cleaned} expired rate limit entries`);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
5 * 60 * 1000,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Prevent the interval from keeping the process alive
|
|
279
|
+
if (this.cleanupInterval.unref) {
|
|
280
|
+
this.cleanupInterval.unref();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|