@lenne.tech/nest-server 11.6.2 → 11.7.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/dist/config.env.js +19 -12
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/filter.helper.d.ts +9 -9
- package/dist/core/common/helpers/filter.helper.js +2 -4
- package/dist/core/common/helpers/filter.helper.js.map +1 -1
- package/dist/core/common/helpers/gridfs.helper.js +3 -3
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +21 -3
- package/dist/core/common/services/crud.service.d.ts +16 -16
- package/dist/core/common/services/crud.service.js +1 -1
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.controller.js +28 -2
- 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 +20 -2
- 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-models.d.ts +0 -1
- package/dist/core/modules/better-auth/better-auth-models.js +0 -4
- package/dist/core/modules/better-auth/better-auth-models.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 +443 -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 +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
- package/dist/core/modules/better-auth/better-auth.module.js +40 -52
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
- package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
- package/dist/core/modules/better-auth/better-auth.service.js +0 -3
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
- package/dist/core/modules/better-auth/better-auth.types.js +14 -3
- 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 +67 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +504 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +61 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +552 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +3 -0
- package/dist/core/modules/better-auth/index.js +3 -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 +132 -54
- 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.controller.d.ts +10 -0
- package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
- package/dist/server/modules/better-auth/better-auth.module.js +44 -0
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +47 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +234 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
- package/dist/server/modules/file/file-info.model.d.ts +71 -3
- package/dist/server/modules/user/user.model.d.ts +169 -3
- 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/server/server.module.js +6 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +20 -29
- package/src/config.env.ts +34 -13
- package/src/core/common/helpers/filter.helper.ts +15 -17
- package/src/core/common/helpers/gridfs.helper.ts +5 -5
- package/src/core/common/interfaces/server-options.interface.ts +222 -14
- package/src/core/common/services/crud.service.ts +22 -22
- package/src/core/modules/auth/core-auth.controller.ts +93 -5
- package/src/core/modules/auth/core-auth.module.ts +15 -1
- package/src/core/modules/auth/core-auth.resolver.ts +70 -2
- 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 +254 -0
- package/src/core/modules/better-auth/README.md +698 -54
- package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
- package/src/core/modules/better-auth/better-auth-models.ts +0 -3
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
- package/src/core/modules/better-auth/better-auth.config.ts +5 -0
- package/src/core/modules/better-auth/better-auth.module.ts +107 -66
- package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
- package/src/core/modules/better-auth/better-auth.service.ts +0 -9
- package/src/core/modules/better-auth/better-auth.types.ts +25 -10
- package/src/core/modules/better-auth/core-better-auth.controller.ts +646 -0
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +730 -0
- package/src/core/modules/better-auth/index.ts +9 -1
- 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 +257 -74
- package/src/index.ts +5 -0
- package/src/server/modules/auth/auth.resolver.ts +8 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
- package/src/server/modules/better-auth/better-auth.module.ts +88 -0
- package/src/server/modules/better-auth/better-auth.resolver.ts +210 -0
- package/src/server/modules/user/user.service.ts +4 -2
- package/src/server/server.module.ts +10 -1
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
* - Two-Factor Authentication (TOTP)
|
|
9
9
|
* - Passkey/WebAuthn authentication
|
|
10
10
|
* - Social Login (Google, GitHub, Apple)
|
|
11
|
-
* - Legacy password handling for migration
|
|
12
11
|
* - Rate limiting for brute-force protection
|
|
12
|
+
* - Parallel operation with Legacy Auth (bcrypt compatible)
|
|
13
|
+
*
|
|
14
|
+
* Extension points:
|
|
15
|
+
* - CoreBetterAuthController: Abstract controller for REST extension
|
|
16
|
+
* - CoreBetterAuthResolver: Abstract resolver for GraphQL extension (isAbstract: true)
|
|
17
|
+
* - BetterAuthController/BetterAuthResolver: Default implementations
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
export * from './better-auth-auth.model';
|
|
21
|
+
export * from './better-auth-migration-status.model';
|
|
16
22
|
export * from './better-auth-models';
|
|
17
23
|
export * from './better-auth-rate-limit.middleware';
|
|
18
24
|
export * from './better-auth-rate-limiter.service';
|
|
@@ -23,3 +29,5 @@ export * from './better-auth.module';
|
|
|
23
29
|
export * from './better-auth.resolver';
|
|
24
30
|
export * from './better-auth.service';
|
|
25
31
|
export * from './better-auth.types';
|
|
32
|
+
export * from './core-better-auth.controller';
|
|
33
|
+
export * from './core-better-auth.resolver';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BadRequestException, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Logger, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
|
|
2
2
|
import bcrypt = require('bcrypt');
|
|
3
3
|
import crypto = require('crypto');
|
|
4
4
|
import { sha256 } from 'js-sha256';
|
|
@@ -13,20 +13,31 @@ import { CoreModelConstructor } from '../../common/types/core-model-constructor.
|
|
|
13
13
|
import { CoreUserModel } from './core-user.model';
|
|
14
14
|
import { CoreUserCreateInput } from './inputs/core-user-create.input';
|
|
15
15
|
import { CoreUserInput } from './inputs/core-user.input';
|
|
16
|
+
import { CoreUserServiceOptions } from './interfaces/core-user-service-options.interface';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* User service
|
|
20
|
+
*
|
|
21
|
+
* Provides user management with automatic synchronization between
|
|
22
|
+
* Legacy Auth and Better-Auth (IAM) systems when both are enabled.
|
|
19
23
|
*/
|
|
20
24
|
export abstract class CoreUserService<
|
|
21
25
|
TUser extends CoreUserModel,
|
|
22
26
|
TUserInput extends CoreUserInput,
|
|
23
27
|
TUserCreateInput extends CoreUserCreateInput,
|
|
24
28
|
> extends CrudService<TUser, TUserCreateInput, TUserInput> {
|
|
29
|
+
protected readonly userServiceLogger = new Logger(CoreUserService.name);
|
|
30
|
+
|
|
25
31
|
protected constructor(
|
|
26
32
|
protected override readonly configService: ConfigService,
|
|
27
33
|
protected readonly emailService: EmailService,
|
|
28
34
|
protected override readonly mainDbModel: Model<Document & TUser>,
|
|
29
35
|
protected override readonly mainModelConstructor: CoreModelConstructor<TUser>,
|
|
36
|
+
/**
|
|
37
|
+
* Optional configuration for additional features like IAM sync.
|
|
38
|
+
* Using options object pattern for extensibility without breaking changes.
|
|
39
|
+
*/
|
|
40
|
+
protected readonly options?: CoreUserServiceOptions,
|
|
30
41
|
) {
|
|
31
42
|
super();
|
|
32
43
|
}
|
|
@@ -125,6 +136,10 @@ export abstract class CoreUserService<
|
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
138
|
* Set new password for user with token
|
|
139
|
+
*
|
|
140
|
+
* This method also syncs the password change to Better-Auth (IAM) if:
|
|
141
|
+
* - BetterAuthUserMapper is configured via options
|
|
142
|
+
* - User has an existing IAM credential account
|
|
128
143
|
*/
|
|
129
144
|
async resetPassword(token: string, newPassword: string, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
130
145
|
// Get user
|
|
@@ -133,6 +148,10 @@ export abstract class CoreUserService<
|
|
|
133
148
|
throw new NotFoundException(`No user found with password reset token: ${token}`);
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
// Store the original plain password for IAM sync before any hashing
|
|
152
|
+
// We need the plain password because IAM uses scrypt, not bcrypt+sha256
|
|
153
|
+
const plainPasswordForIamSync = /^[a-f0-9]{64}$/i.test(newPassword) ? undefined : newPassword;
|
|
154
|
+
|
|
136
155
|
return this.process(
|
|
137
156
|
async () => {
|
|
138
157
|
// Check if the password was transmitted encrypted
|
|
@@ -141,11 +160,26 @@ export abstract class CoreUserService<
|
|
|
141
160
|
newPassword = sha256(newPassword);
|
|
142
161
|
}
|
|
143
162
|
|
|
144
|
-
// Update
|
|
145
|
-
|
|
163
|
+
// Update Legacy Auth password
|
|
164
|
+
const updatedUser = await assignPlain(dbObject, {
|
|
146
165
|
password: await bcrypt.hash(newPassword, 10),
|
|
147
166
|
passwordResetToken: null,
|
|
148
167
|
}).save();
|
|
168
|
+
|
|
169
|
+
// Sync password to Better-Auth (IAM) if mapper is available
|
|
170
|
+
// This ensures users can sign in via IAM after password reset
|
|
171
|
+
if (this.options?.betterAuthUserMapper && plainPasswordForIamSync && dbObject.email) {
|
|
172
|
+
try {
|
|
173
|
+
await this.options.betterAuthUserMapper.syncPasswordChangeToIam(dbObject.email, plainPasswordForIamSync);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Log but don't fail - Legacy Auth password was updated successfully
|
|
176
|
+
this.userServiceLogger.warn(
|
|
177
|
+
`Failed to sync password reset to IAM for ${dbObject.email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return updatedUser;
|
|
149
183
|
},
|
|
150
184
|
{ dbObject, serviceOptions },
|
|
151
185
|
);
|
|
@@ -186,7 +220,7 @@ export abstract class CoreUserService<
|
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
// Check roles values
|
|
189
|
-
if (roles.some(role => typeof role !== 'string')) {
|
|
223
|
+
if (roles.some((role) => typeof role !== 'string')) {
|
|
190
224
|
throw new BadRequestException('Roles contains invalid values');
|
|
191
225
|
}
|
|
192
226
|
|
|
@@ -198,4 +232,97 @@ export abstract class CoreUserService<
|
|
|
198
232
|
{ serviceOptions },
|
|
199
233
|
);
|
|
200
234
|
}
|
|
235
|
+
|
|
236
|
+
// ===================================================================================================================
|
|
237
|
+
// Auth System Sync Methods
|
|
238
|
+
// ===================================================================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Update user with automatic email and password sync between Legacy and IAM auth systems
|
|
242
|
+
*
|
|
243
|
+
* When the email changes and BetterAuthUserMapper is available, this method:
|
|
244
|
+
* - Invalidates all Better-Auth sessions (forces re-authentication)
|
|
245
|
+
* - The shared users collection is automatically updated
|
|
246
|
+
*
|
|
247
|
+
* When the password changes:
|
|
248
|
+
* - Updates the Legacy Auth password (bcrypt hash)
|
|
249
|
+
* - Syncs to Better-Auth (IAM) if the user has a credential account
|
|
250
|
+
*/
|
|
251
|
+
override async update(id: string, input: TUserInput, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
252
|
+
// Get the current user before update to detect email changes
|
|
253
|
+
const oldUser = (await this.mainDbModel.findById(id).lean().exec()) as null | TUser;
|
|
254
|
+
const oldEmail = oldUser?.email;
|
|
255
|
+
|
|
256
|
+
// Store plain password for IAM sync before any hashing occurs
|
|
257
|
+
// We need to capture this before super.update() which may hash it
|
|
258
|
+
const inputPassword = (input as any).password;
|
|
259
|
+
const plainPasswordForIamSync = inputPassword && !/^[a-f0-9]{64}$/i.test(inputPassword) ? inputPassword : undefined;
|
|
260
|
+
|
|
261
|
+
// Perform the update
|
|
262
|
+
const updatedUser = await super.update(id, input, serviceOptions);
|
|
263
|
+
|
|
264
|
+
// Sync email change to IAM if email was changed and mapper is available
|
|
265
|
+
if (this.options?.betterAuthUserMapper && oldEmail && input.email && oldEmail !== input.email) {
|
|
266
|
+
try {
|
|
267
|
+
await this.options.betterAuthUserMapper.syncEmailChangeFromLegacy(oldEmail, input.email);
|
|
268
|
+
this.userServiceLogger.debug(`Synced email change from Legacy to IAM: ${oldEmail} → ${input.email}`);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
this.userServiceLogger.error(
|
|
271
|
+
`Failed to sync email change to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
272
|
+
);
|
|
273
|
+
// Don't throw - email sync failure shouldn't block the update
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Sync password change to IAM if password was changed and mapper is available
|
|
278
|
+
if (this.options?.betterAuthUserMapper && plainPasswordForIamSync && oldUser?.email) {
|
|
279
|
+
try {
|
|
280
|
+
await this.options.betterAuthUserMapper.syncPasswordChangeToIam(oldUser.email, plainPasswordForIamSync);
|
|
281
|
+
this.userServiceLogger.debug(`Synced password change to IAM for user ${oldUser.email}`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.userServiceLogger.warn(
|
|
284
|
+
`Failed to sync password change to IAM for ${oldUser.email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
285
|
+
);
|
|
286
|
+
// Don't throw - password sync failure shouldn't block the update
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return updatedUser;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Delete user with automatic cleanup of IAM auth data
|
|
295
|
+
*
|
|
296
|
+
* When BetterAuthUserMapper is available, this method also:
|
|
297
|
+
* - Deletes all Better-Auth accounts for this user
|
|
298
|
+
* - Deletes all Better-Auth sessions for this user
|
|
299
|
+
*
|
|
300
|
+
* This ensures no orphaned auth data remains after user deletion.
|
|
301
|
+
*/
|
|
302
|
+
override async delete(id: string, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
303
|
+
// Get the user before deletion to cleanup IAM data
|
|
304
|
+
const user = (await this.mainDbModel.findById(id).lean().exec()) as null | (TUser & { _id: any });
|
|
305
|
+
|
|
306
|
+
// Perform the deletion
|
|
307
|
+
const deletedUser = await super.delete(id, serviceOptions);
|
|
308
|
+
|
|
309
|
+
// Cleanup IAM data if mapper is available
|
|
310
|
+
if (this.options?.betterAuthUserMapper && user?._id) {
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.options.betterAuthUserMapper.cleanupIamDataForDeletedUser(user._id);
|
|
313
|
+
if (result.accountsDeleted > 0 || result.sessionsDeleted > 0) {
|
|
314
|
+
this.userServiceLogger.debug(
|
|
315
|
+
`Cleaned up IAM data for deleted user ${id}: accounts=${result.accountsDeleted}, sessions=${result.sessionsDeleted}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.userServiceLogger.error(
|
|
320
|
+
`Failed to cleanup IAM data for deleted user: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
321
|
+
);
|
|
322
|
+
// Don't throw - cleanup failure shouldn't block the delete response
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return deletedUser;
|
|
327
|
+
}
|
|
201
328
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional configuration for CoreUserService
|
|
5
|
+
*
|
|
6
|
+
* Use this interface for optional dependencies that may not be available in all projects.
|
|
7
|
+
* This pattern allows adding new optional parameters without breaking existing implementations.
|
|
8
|
+
*/
|
|
9
|
+
export interface CoreUserServiceOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Optional BetterAuthUserMapper for syncing between Legacy and IAM auth systems.
|
|
12
|
+
* When provided, email changes and user deletions are automatically synced.
|
|
13
|
+
*/
|
|
14
|
+
betterAuthUserMapper?: BetterAuthUserMapper;
|
|
15
|
+
}
|
package/src/core.module.ts
CHANGED
|
@@ -19,7 +19,9 @@ import { EmailService } from './core/common/services/email.service';
|
|
|
19
19
|
import { MailjetService } from './core/common/services/mailjet.service';
|
|
20
20
|
import { ModelDocService } from './core/common/services/model-doc.service';
|
|
21
21
|
import { TemplateService } from './core/common/services/template.service';
|
|
22
|
+
import { BetterAuthUserMapper } from './core/modules/better-auth/better-auth-user.mapper';
|
|
22
23
|
import { BetterAuthModule } from './core/modules/better-auth/better-auth.module';
|
|
24
|
+
import { BetterAuthService } from './core/modules/better-auth/better-auth.service';
|
|
23
25
|
import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -66,10 +68,65 @@ export class CoreModule implements NestModule {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Dynamic module
|
|
70
|
-
*
|
|
71
|
+
* Dynamic module initialization
|
|
72
|
+
*
|
|
73
|
+
* @see https://docs.nestjs.com/modules#dynamic-modules
|
|
74
|
+
*
|
|
75
|
+
* ## Signatures
|
|
76
|
+
*
|
|
77
|
+
* ### IAM-Only Signature (Recommended for new projects)
|
|
78
|
+
*
|
|
79
|
+
* ```typescript
|
|
80
|
+
* CoreModule.forRoot(envConfig)
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Use this for new projects that only use BetterAuth (IAM) for authentication.
|
|
84
|
+
* GraphQL Subscription authentication uses BetterAuth JWT tokens.
|
|
85
|
+
*
|
|
86
|
+
* **Requirements:**
|
|
87
|
+
* - Configure `betterAuth` in your config (enabled by default)
|
|
88
|
+
* - Create BetterAuthModule, Resolver, and Controller in your project
|
|
89
|
+
* - Inject BetterAuthUserMapper in UserService
|
|
90
|
+
*
|
|
91
|
+
* ### Legacy + IAM Signature (For existing projects)
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig)
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @deprecated This 3-parameter signature is deprecated for new projects.
|
|
98
|
+
* Use the single-parameter signature `CoreModule.forRoot(envConfig)` instead.
|
|
99
|
+
* Existing projects can continue using this signature during migration.
|
|
100
|
+
*
|
|
101
|
+
* Use this for existing projects that need Legacy Auth for backwards compatibility.
|
|
102
|
+
* Both Legacy Auth and BetterAuth (IAM) can run in parallel.
|
|
103
|
+
*
|
|
104
|
+
* ## Migration Path
|
|
105
|
+
*
|
|
106
|
+
* 1. **Existing projects**: Use the 3-parameter signature, run Legacy + IAM in parallel
|
|
107
|
+
* 2. **Monitor**: Use `betterAuthMigrationStatus` query to track user migration
|
|
108
|
+
* 3. **Disable Legacy**: Set `auth.legacyEndpoints.enabled: false` after all users migrated
|
|
109
|
+
* 4. **New projects**: Use the single-parameter signature with IAM only
|
|
110
|
+
*
|
|
111
|
+
* @see https://github.com/lenneTech/nest-server/blob/develop/.claude/rules/module-deprecation.md
|
|
71
112
|
*/
|
|
72
|
-
static forRoot(
|
|
113
|
+
static forRoot(options: Partial<IServerOptions>): DynamicModule;
|
|
114
|
+
/**
|
|
115
|
+
* @deprecated Use the single-parameter signature `CoreModule.forRoot(envConfig)` for new projects.
|
|
116
|
+
* This 3-parameter signature is for existing projects during migration to IAM.
|
|
117
|
+
*/
|
|
118
|
+
static forRoot(AuthService: any, AuthModule: any, options: Partial<IServerOptions>): DynamicModule;
|
|
119
|
+
static forRoot(
|
|
120
|
+
authServiceOrOptions: any,
|
|
121
|
+
authModuleOrUndefined?: any,
|
|
122
|
+
optionsOrUndefined?: Partial<IServerOptions>,
|
|
123
|
+
): DynamicModule {
|
|
124
|
+
// Detect which signature was used
|
|
125
|
+
const isIamOnlyMode = authModuleOrUndefined === undefined && optionsOrUndefined === undefined;
|
|
126
|
+
const AuthService = isIamOnlyMode ? null : authServiceOrOptions;
|
|
127
|
+
const AuthModule = isIamOnlyMode ? null : authModuleOrUndefined;
|
|
128
|
+
const options: Partial<IServerOptions> = isIamOnlyMode ? authServiceOrOptions : optionsOrUndefined;
|
|
129
|
+
|
|
73
130
|
// Process config
|
|
74
131
|
let cors = {};
|
|
75
132
|
if (options?.cookies) {
|
|
@@ -78,73 +135,17 @@ export class CoreModule implements NestModule {
|
|
|
78
135
|
origin: true,
|
|
79
136
|
};
|
|
80
137
|
}
|
|
138
|
+
|
|
139
|
+
// Build GraphQL driver configuration based on auth mode
|
|
140
|
+
const graphQlDriverConfig = isIamOnlyMode
|
|
141
|
+
? this.buildIamOnlyGraphQlDriver(cors, options)
|
|
142
|
+
: this.buildLegacyGraphQlDriver(AuthService, AuthModule, cors, options);
|
|
143
|
+
|
|
81
144
|
const config: IServerOptions = merge(
|
|
82
145
|
{
|
|
83
146
|
env: 'develop',
|
|
84
147
|
graphQl: {
|
|
85
|
-
driver:
|
|
86
|
-
imports: [AuthModule],
|
|
87
|
-
inject: [AuthService],
|
|
88
|
-
useFactory: async (authService: any) =>
|
|
89
|
-
Object.assign(
|
|
90
|
-
{
|
|
91
|
-
autoSchemaFile: 'schema.gql',
|
|
92
|
-
context: ({ req, res }) => ({ req, res }),
|
|
93
|
-
cors,
|
|
94
|
-
installSubscriptionHandlers: true,
|
|
95
|
-
subscriptions: {
|
|
96
|
-
'graphql-ws': {
|
|
97
|
-
context: ({ extra }) => extra,
|
|
98
|
-
onConnect: async (context: Context<any>) => {
|
|
99
|
-
const { connectionParams, extra } = context;
|
|
100
|
-
if (config.graphQl.enableSubscriptionAuth) {
|
|
101
|
-
// get authToken from authorization header
|
|
102
|
-
const headers = this.getHeaderFromArray(extra.request?.rawHeaders);
|
|
103
|
-
const authToken: string =
|
|
104
|
-
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
105
|
-
if (authToken) {
|
|
106
|
-
// verify authToken/getJwtPayLoad
|
|
107
|
-
const payload = authService.decodeJwt(authToken);
|
|
108
|
-
const user = await authService.validateUser(payload);
|
|
109
|
-
if (!user) {
|
|
110
|
-
throw new UnauthorizedException('No user found for token');
|
|
111
|
-
}
|
|
112
|
-
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
113
|
-
extra.user = user;
|
|
114
|
-
extra.headers = connectionParams ?? headers;
|
|
115
|
-
return extra;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new UnauthorizedException('Missing authentication token');
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
'subscriptions-transport-ws': {
|
|
123
|
-
onConnect: async (connectionParams) => {
|
|
124
|
-
if (config.graphQl.enableSubscriptionAuth) {
|
|
125
|
-
// get authToken from authorization header
|
|
126
|
-
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
127
|
-
|
|
128
|
-
if (authToken) {
|
|
129
|
-
// verify authToken/getJwtPayLoad
|
|
130
|
-
const payload = authService.decodeJwt(authToken);
|
|
131
|
-
const user = await authService.validateUser(payload);
|
|
132
|
-
if (!user) {
|
|
133
|
-
throw new UnauthorizedException('No user found for token');
|
|
134
|
-
}
|
|
135
|
-
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
136
|
-
return { headers: connectionParams, user };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
throw new UnauthorizedException('Missing authentication token');
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
options?.graphQl?.driver,
|
|
146
|
-
),
|
|
147
|
-
},
|
|
148
|
+
driver: graphQlDriverConfig,
|
|
148
149
|
enableSubscriptionAuth: true,
|
|
149
150
|
},
|
|
150
151
|
mongoose: {
|
|
@@ -229,15 +230,19 @@ export class CoreModule implements NestModule {
|
|
|
229
230
|
imports.push(CoreHealthCheckModule);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
// Add BetterAuthModule
|
|
233
|
+
// Add BetterAuthModule based on mode
|
|
234
|
+
// IAM-only mode: Always register BetterAuthModule (required for subscription auth)
|
|
235
|
+
// Legacy mode: Only register if autoRegister is explicitly true
|
|
233
236
|
if (config.betterAuth?.enabled !== false) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
if (isIamOnlyMode || config.betterAuth?.autoRegister === true) {
|
|
238
|
+
imports.push(
|
|
239
|
+
BetterAuthModule.forRoot({
|
|
240
|
+
config: config.betterAuth || {},
|
|
241
|
+
// Pass JWT secrets for backwards compatibility fallback
|
|
242
|
+
fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
// Set exports
|
|
@@ -254,4 +259,182 @@ export class CoreModule implements NestModule {
|
|
|
254
259
|
providers,
|
|
255
260
|
};
|
|
256
261
|
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// GraphQL Driver Configuration Helpers
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build GraphQL driver configuration for IAM-only mode
|
|
269
|
+
*
|
|
270
|
+
* Uses BetterAuthService for subscription authentication via JWT tokens.
|
|
271
|
+
* This is the recommended mode for new projects.
|
|
272
|
+
*/
|
|
273
|
+
private static buildIamOnlyGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
|
|
274
|
+
return {
|
|
275
|
+
imports: [BetterAuthModule],
|
|
276
|
+
inject: [BetterAuthService, BetterAuthUserMapper],
|
|
277
|
+
useFactory: async (betterAuthService: BetterAuthService, userMapper: BetterAuthUserMapper) =>
|
|
278
|
+
Object.assign(
|
|
279
|
+
{
|
|
280
|
+
autoSchemaFile: 'schema.gql',
|
|
281
|
+
context: ({ req, res }) => ({ req, res }),
|
|
282
|
+
cors,
|
|
283
|
+
installSubscriptionHandlers: true,
|
|
284
|
+
subscriptions: {
|
|
285
|
+
'graphql-ws': {
|
|
286
|
+
context: ({ extra }) => extra,
|
|
287
|
+
onConnect: async (context: Context<any>) => {
|
|
288
|
+
const { connectionParams, extra } = context;
|
|
289
|
+
const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
|
|
290
|
+
|
|
291
|
+
if (enableAuth) {
|
|
292
|
+
// Get headers from raw headers or connection params
|
|
293
|
+
const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
|
|
294
|
+
const authToken: string =
|
|
295
|
+
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
296
|
+
|
|
297
|
+
if (authToken) {
|
|
298
|
+
// Validate via BetterAuth session
|
|
299
|
+
const { session, user: sessionUser } = await betterAuthService.getSession({
|
|
300
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (!session || !sessionUser) {
|
|
304
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Map to full user with roles
|
|
308
|
+
const user = await userMapper.mapSessionUser(sessionUser);
|
|
309
|
+
if (!user) {
|
|
310
|
+
throw new UnauthorizedException('User not found');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
extra.user = user;
|
|
314
|
+
extra.headers = connectionParams ?? headers;
|
|
315
|
+
return extra;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw new UnauthorizedException('Missing authentication token');
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
'subscriptions-transport-ws': {
|
|
323
|
+
onConnect: async (connectionParams) => {
|
|
324
|
+
const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
|
|
325
|
+
|
|
326
|
+
if (enableAuth) {
|
|
327
|
+
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
328
|
+
|
|
329
|
+
if (authToken) {
|
|
330
|
+
// Validate via BetterAuth session
|
|
331
|
+
const { session, user: sessionUser } = await betterAuthService.getSession({
|
|
332
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!session || !sessionUser) {
|
|
336
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Map to full user with roles
|
|
340
|
+
const user = await userMapper.mapSessionUser(sessionUser);
|
|
341
|
+
if (!user) {
|
|
342
|
+
throw new UnauthorizedException('User not found');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { headers: connectionParams, user };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new UnauthorizedException('Missing authentication token');
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
options?.graphQl?.driver,
|
|
355
|
+
),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Build GraphQL driver configuration for Legacy Auth mode
|
|
361
|
+
*
|
|
362
|
+
* Uses the provided AuthService for subscription authentication via JWT tokens.
|
|
363
|
+
* This is for existing projects that need backwards compatibility.
|
|
364
|
+
*
|
|
365
|
+
* @deprecated Use IAM-only mode (single-parameter forRoot) for new projects
|
|
366
|
+
*/
|
|
367
|
+
private static buildLegacyGraphQlDriver(
|
|
368
|
+
AuthService: any,
|
|
369
|
+
AuthModule: any,
|
|
370
|
+
cors: object,
|
|
371
|
+
options: Partial<IServerOptions>,
|
|
372
|
+
) {
|
|
373
|
+
// Store config reference for use in callbacks
|
|
374
|
+
const enableSubscriptionAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
imports: [AuthModule],
|
|
378
|
+
inject: [AuthService],
|
|
379
|
+
useFactory: async (authService: any) =>
|
|
380
|
+
Object.assign(
|
|
381
|
+
{
|
|
382
|
+
autoSchemaFile: 'schema.gql',
|
|
383
|
+
context: ({ req, res }) => ({ req, res }),
|
|
384
|
+
cors,
|
|
385
|
+
installSubscriptionHandlers: true,
|
|
386
|
+
subscriptions: {
|
|
387
|
+
'graphql-ws': {
|
|
388
|
+
context: ({ extra }) => extra,
|
|
389
|
+
onConnect: async (context: Context<any>) => {
|
|
390
|
+
const { connectionParams, extra } = context;
|
|
391
|
+
if (enableSubscriptionAuth) {
|
|
392
|
+
// get authToken from authorization header
|
|
393
|
+
const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
|
|
394
|
+
const authToken: string =
|
|
395
|
+
connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
|
|
396
|
+
if (authToken) {
|
|
397
|
+
// verify authToken/getJwtPayLoad
|
|
398
|
+
const payload = authService.decodeJwt(authToken);
|
|
399
|
+
const user = await authService.validateUser(payload);
|
|
400
|
+
if (!user) {
|
|
401
|
+
throw new UnauthorizedException('No user found for token');
|
|
402
|
+
}
|
|
403
|
+
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
404
|
+
extra.user = user;
|
|
405
|
+
extra.headers = connectionParams ?? headers;
|
|
406
|
+
return extra;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
throw new UnauthorizedException('Missing authentication token');
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
'subscriptions-transport-ws': {
|
|
414
|
+
onConnect: async (connectionParams) => {
|
|
415
|
+
if (enableSubscriptionAuth) {
|
|
416
|
+
// get authToken from authorization header
|
|
417
|
+
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
418
|
+
|
|
419
|
+
if (authToken) {
|
|
420
|
+
// verify authToken/getJwtPayLoad
|
|
421
|
+
const payload = authService.decodeJwt(authToken);
|
|
422
|
+
const user = await authService.validateUser(payload);
|
|
423
|
+
if (!user) {
|
|
424
|
+
throw new UnauthorizedException('No user found for token');
|
|
425
|
+
}
|
|
426
|
+
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
|
|
427
|
+
return { headers: connectionParams, user };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
throw new UnauthorizedException('Missing authentication token');
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
options?.graphQl?.driver,
|
|
437
|
+
),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
257
440
|
}
|
package/src/index.ts
CHANGED
|
@@ -108,15 +108,19 @@ export * from './core/modules/auth/core-auth.resolver';
|
|
|
108
108
|
export * from './core/modules/auth/exceptions/expired-refresh-token.exception';
|
|
109
109
|
export * from './core/modules/auth/exceptions/expired-token.exception';
|
|
110
110
|
export * from './core/modules/auth/exceptions/invalid-token.exception';
|
|
111
|
+
export * from './core/modules/auth/exceptions/legacy-auth-disabled.exception';
|
|
111
112
|
export * from './core/modules/auth/guards/auth.guard';
|
|
113
|
+
export * from './core/modules/auth/guards/legacy-auth-rate-limit.guard';
|
|
112
114
|
export * from './core/modules/auth/guards/roles.guard';
|
|
113
115
|
export * from './core/modules/auth/inputs/core-auth-sign-in.input';
|
|
114
116
|
export * from './core/modules/auth/inputs/core-auth-sign-up.input';
|
|
117
|
+
export * from './core/modules/auth/interfaces/auth-provider.interface';
|
|
115
118
|
export * from './core/modules/auth/interfaces/core-auth-user.interface';
|
|
116
119
|
export * from './core/modules/auth/interfaces/core-token-data.interface';
|
|
117
120
|
export * from './core/modules/auth/interfaces/jwt-payload.interface';
|
|
118
121
|
export * from './core/modules/auth/services/core-auth-user.service';
|
|
119
122
|
export * from './core/modules/auth/services/core-auth.service';
|
|
123
|
+
export * from './core/modules/auth/services/legacy-auth-rate-limiter.service';
|
|
120
124
|
export * from './core/modules/auth/strategies/jwt-refresh.strategy';
|
|
121
125
|
export * from './core/modules/auth/strategies/jwt.strategy';
|
|
122
126
|
export * from './core/modules/auth/tokens.decorator';
|
|
@@ -162,6 +166,7 @@ export * from './core/modules/user/core-user.model';
|
|
|
162
166
|
export * from './core/modules/user/core-user.service';
|
|
163
167
|
export * from './core/modules/user/inputs/core-user-create.input';
|
|
164
168
|
export * from './core/modules/user/inputs/core-user.input';
|
|
169
|
+
export * from './core/modules/user/interfaces/core-user-service-options.interface';
|
|
165
170
|
|
|
166
171
|
// =====================================================================================================================
|
|
167
172
|
// Tests
|
|
@@ -30,6 +30,8 @@ export class AuthResolver extends CoreAuthResolver {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* SignIn for User
|
|
33
|
+
*
|
|
34
|
+
* @throws LegacyAuthDisabledException if legacy endpoints are disabled
|
|
33
35
|
*/
|
|
34
36
|
@Mutation(() => Auth, { description: 'Sign in and get JWT token' })
|
|
35
37
|
@Roles(RoleEnum.S_EVERYONE)
|
|
@@ -38,6 +40,8 @@ export class AuthResolver extends CoreAuthResolver {
|
|
|
38
40
|
@Context() ctx: { res: ResponseType },
|
|
39
41
|
@Args('input') input: AuthSignInInput,
|
|
40
42
|
): Promise<Auth> {
|
|
43
|
+
// Check if legacy endpoints are enabled before proceeding
|
|
44
|
+
this.checkLegacyGraphQLEnabled('signIn');
|
|
41
45
|
const result = await this.authService.signIn(input, {
|
|
42
46
|
...serviceOptions,
|
|
43
47
|
inputType: AuthSignInInput,
|
|
@@ -47,6 +51,8 @@ export class AuthResolver extends CoreAuthResolver {
|
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* Sign up for user
|
|
54
|
+
*
|
|
55
|
+
* @throws LegacyAuthDisabledException if legacy endpoints are disabled
|
|
50
56
|
*/
|
|
51
57
|
@Mutation(() => Auth, {
|
|
52
58
|
description: 'Sign up user and get JWT token',
|
|
@@ -57,6 +63,8 @@ export class AuthResolver extends CoreAuthResolver {
|
|
|
57
63
|
@Context() ctx: { res: ResponseType },
|
|
58
64
|
@Args('input') input: AuthSignUpInput,
|
|
59
65
|
): Promise<Auth> {
|
|
66
|
+
// Check if legacy endpoints are enabled before proceeding
|
|
67
|
+
this.checkLegacyGraphQLEnabled('signUp');
|
|
60
68
|
const result = await this.authService.signUp(input, serviceOptions);
|
|
61
69
|
return this.processCookies(ctx, result);
|
|
62
70
|
}
|