@lenne.tech/nest-server 11.9.0 → 11.10.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.
Files changed (131) hide show
  1. package/dist/config.env.js +2 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/logging.helper.d.ts +6 -0
  4. package/dist/core/common/helpers/logging.helper.js +55 -0
  5. package/dist/core/common/helpers/logging.helper.js.map +1 -0
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +37 -19
  7. package/dist/core/modules/auth/guards/roles.guard.js +33 -2
  8. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  9. package/dist/core/modules/auth/services/core-auth.service.d.ts +5 -5
  10. package/dist/core/modules/auth/services/core-auth.service.js +4 -4
  11. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  12. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  13. package/dist/core/modules/better-auth/better-auth.config.js +32 -10
  14. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +16 -16
  16. package/dist/core/modules/better-auth/better-auth.resolver.js +34 -34
  17. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  18. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -1
  19. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +10 -0
  21. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +91 -0
  22. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -0
  23. package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +9 -0
  24. package/dist/core/modules/better-auth/{better-auth-auth.model.js → core-better-auth-auth.model.js} +17 -17
  25. package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -0
  26. package/dist/core/modules/better-auth/{better-auth-migration-status.model.d.ts → core-better-auth-migration-status.model.d.ts} +1 -1
  27. package/dist/core/modules/better-auth/{better-auth-migration-status.model.js → core-better-auth-migration-status.model.js} +14 -14
  28. package/dist/core/modules/better-auth/core-better-auth-migration-status.model.js.map +1 -0
  29. package/dist/core/modules/better-auth/{better-auth-models.d.ts → core-better-auth-models.d.ts} +8 -8
  30. package/dist/core/modules/better-auth/{better-auth-models.js → core-better-auth-models.js} +61 -61
  31. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -0
  32. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +12 -0
  33. package/dist/core/modules/better-auth/{better-auth-rate-limit.middleware.js → core-better-auth-rate-limit.middleware.js} +10 -10
  34. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -0
  35. package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.d.ts → core-better-auth-rate-limiter.service.d.ts} +1 -1
  36. package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.js → core-better-auth-rate-limiter.service.js} +8 -8
  37. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -0
  38. package/dist/core/modules/better-auth/{better-auth-user.mapper.d.ts → core-better-auth-user.mapper.d.ts} +1 -1
  39. package/dist/core/modules/better-auth/{better-auth-user.mapper.js → core-better-auth-user.mapper.js} +10 -9
  40. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -0
  41. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +19 -0
  42. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +152 -0
  43. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -0
  44. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +23 -32
  45. package/dist/core/modules/better-auth/core-better-auth.controller.js +184 -201
  46. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  47. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +22 -0
  48. package/dist/core/modules/better-auth/{better-auth.middleware.js → core-better-auth.middleware.js} +45 -18
  49. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -0
  50. package/dist/core/modules/better-auth/{better-auth.module.d.ts → core-better-auth.module.d.ts} +6 -6
  51. package/dist/core/modules/better-auth/{better-auth.module.js → core-better-auth.module.js} +65 -60
  52. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -0
  53. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +19 -19
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.js +18 -18
  55. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  56. package/dist/core/modules/better-auth/{better-auth.service.d.ts → core-better-auth.service.d.ts} +3 -2
  57. package/dist/core/modules/better-auth/{better-auth.service.js → core-better-auth.service.js} +75 -35
  58. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -0
  59. package/dist/core/modules/better-auth/index.d.ts +11 -9
  60. package/dist/core/modules/better-auth/index.js +11 -9
  61. package/dist/core/modules/better-auth/index.js.map +1 -1
  62. package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -1
  63. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +2 -2
  64. package/dist/core.module.js +6 -6
  65. package/dist/core.module.js.map +1 -1
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/server/modules/better-auth/better-auth.controller.d.ts +5 -5
  70. package/dist/server/modules/better-auth/better-auth.controller.js +4 -4
  71. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
  72. package/dist/server/modules/better-auth/better-auth.module.js +3 -3
  73. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  74. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +17 -17
  75. package/dist/server/modules/better-auth/better-auth.resolver.js +18 -18
  76. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  77. package/dist/server/modules/user/user.service.d.ts +2 -2
  78. package/dist/server/modules/user/user.service.js +2 -2
  79. package/dist/server/modules/user/user.service.js.map +1 -1
  80. package/dist/test/test.helper.d.ts +1 -0
  81. package/dist/test/test.helper.js +5 -1
  82. package/dist/test/test.helper.js.map +1 -1
  83. package/dist/tsconfig.build.tsbuildinfo +1 -1
  84. package/package.json +5 -3
  85. package/src/config.env.ts +15 -0
  86. package/src/core/common/helpers/logging.helper.ts +134 -0
  87. package/src/core/common/interfaces/server-options.interface.ts +419 -234
  88. package/src/core/modules/auth/guards/roles.guard.ts +44 -3
  89. package/src/core/modules/auth/services/core-auth.service.ts +4 -4
  90. package/src/core/modules/better-auth/ARCHITECTURE.md +102 -0
  91. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +277 -8
  92. package/src/core/modules/better-auth/README.md +97 -53
  93. package/src/core/modules/better-auth/better-auth.config.ts +66 -18
  94. package/src/core/modules/better-auth/better-auth.resolver.ts +32 -32
  95. package/src/core/modules/better-auth/better-auth.types.ts +3 -2
  96. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +134 -0
  97. package/src/core/modules/better-auth/{better-auth-auth.model.ts → core-better-auth-auth.model.ts} +6 -6
  98. package/src/core/modules/better-auth/{better-auth-migration-status.model.ts → core-better-auth-migration-status.model.ts} +1 -1
  99. package/src/core/modules/better-auth/{better-auth-models.ts → core-better-auth-models.ts} +9 -9
  100. package/src/core/modules/better-auth/{better-auth-rate-limit.middleware.ts → core-better-auth-rate-limit.middleware.ts} +5 -5
  101. package/src/core/modules/better-auth/{better-auth-rate-limiter.service.ts → core-better-auth-rate-limiter.service.ts} +2 -2
  102. package/src/core/modules/better-auth/{better-auth-user.mapper.ts → core-better-auth-user.mapper.ts} +4 -3
  103. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +272 -0
  104. package/src/core/modules/better-auth/core-better-auth.controller.ts +386 -230
  105. package/src/core/modules/better-auth/{better-auth.middleware.ts → core-better-auth.middleware.ts} +57 -17
  106. package/src/core/modules/better-auth/{better-auth.module.ts → core-better-auth.module.ts} +77 -66
  107. package/src/core/modules/better-auth/core-better-auth.resolver.ts +42 -42
  108. package/src/core/modules/better-auth/{better-auth.service.ts → core-better-auth.service.ts} +86 -40
  109. package/src/core/modules/better-auth/index.ts +18 -11
  110. package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +4 -1
  111. package/src/core/modules/error-code/core-error-code.controller.ts +3 -2
  112. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +3 -3
  113. package/src/core.module.ts +12 -12
  114. package/src/index.ts +1 -0
  115. package/src/server/modules/better-auth/better-auth.controller.ts +4 -4
  116. package/src/server/modules/better-auth/better-auth.module.ts +1 -1
  117. package/src/server/modules/better-auth/better-auth.resolver.ts +31 -31
  118. package/src/server/modules/user/user.service.ts +2 -2
  119. package/src/test/test.helper.ts +13 -1
  120. package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +0 -9
  121. package/dist/core/modules/better-auth/better-auth-auth.model.js.map +0 -1
  122. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +0 -1
  123. package/dist/core/modules/better-auth/better-auth-models.js.map +0 -1
  124. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +0 -12
  125. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +0 -1
  126. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +0 -1
  127. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +0 -1
  128. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +0 -21
  129. package/dist/core/modules/better-auth/better-auth.middleware.js.map +0 -1
  130. package/dist/core/modules/better-auth/better-auth.module.js.map +0 -1
  131. package/dist/core/modules/better-auth/better-auth.service.js.map +0 -1
@@ -6,7 +6,7 @@ import { Connection, Types } from 'mongoose';
6
6
  import { firstValueFrom, isObservable } from 'rxjs';
7
7
 
8
8
  import { RoleEnum } from '../../../common/enums/role.enum';
9
- import { BetterAuthService } from '../../better-auth/better-auth.service';
9
+ import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
10
10
  import { ErrorCode } from '../../error-code';
11
11
  import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
12
12
  import { ExpiredTokenException } from '../exceptions/expired-token.exception';
@@ -36,7 +36,7 @@ import { AuthGuard } from './auth.guard';
36
36
  @Injectable()
37
37
  export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
38
38
  private readonly logger = new Logger(RolesGuard.name);
39
- private betterAuthService: BetterAuthService | null = null;
39
+ private betterAuthService: CoreBetterAuthService | null = null;
40
40
  private mongoConnection: Connection | null = null;
41
41
  private servicesResolved = false;
42
42
 
@@ -59,7 +59,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
59
59
  }
60
60
 
61
61
  try {
62
- this.betterAuthService = this.moduleRef.get(BetterAuthService, { strict: false });
62
+ this.betterAuthService = this.moduleRef.get(CoreBetterAuthService, { strict: false });
63
63
  } catch {
64
64
  // BetterAuth not available - that's fine, we'll use Legacy JWT only
65
65
  }
@@ -168,6 +168,47 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
168
168
  token = authHeader.substring(7);
169
169
  }
170
170
 
171
+ // If no token in header, try cookies (for REST endpoints)
172
+ if (!token) {
173
+ let cookies: Record<string, string> | undefined;
174
+
175
+ // Try GraphQL context first
176
+ try {
177
+ const gqlContext = GqlExecutionContext.create(context);
178
+ const ctx = gqlContext.getContext();
179
+ if (ctx?.req?.cookies) {
180
+ cookies = ctx.req.cookies;
181
+ }
182
+ } catch {
183
+ // GraphQL context not available
184
+ }
185
+
186
+ // Fallback to HTTP context
187
+ if (!cookies) {
188
+ try {
189
+ const httpRequest = context.switchToHttp().getRequest();
190
+ if (httpRequest?.cookies) {
191
+ cookies = httpRequest.cookies;
192
+ }
193
+ } catch {
194
+ // HTTP context not available
195
+ }
196
+ }
197
+
198
+ // Extract session token from cookies (try multiple cookie names)
199
+ if (cookies) {
200
+ // Get the basePath for cookie name (e.g., 'iam' -> 'iam.session_token')
201
+ const basePath = this.betterAuthService.getBasePath?.()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
202
+ const basePathCookie = `${basePath}.session_token`;
203
+
204
+ token =
205
+ cookies[basePathCookie] ||
206
+ cookies['better-auth.session_token'] ||
207
+ cookies['token'] ||
208
+ undefined;
209
+ }
210
+ }
211
+
171
212
  if (!token) {
172
213
  return null;
173
214
  }
@@ -15,8 +15,8 @@ import { getStringIds } from '../../../common/helpers/db.helper';
15
15
  import { prepareServiceOptions } from '../../../common/helpers/service.helper';
16
16
  import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
17
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';
18
+ import { CoreBetterAuthUserMapper } from '../../better-auth/core-better-auth-user.mapper';
19
+ import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
20
20
  import { ErrorCode } from '../../error-code';
21
21
  import { CoreAuthModel } from '../core-auth.model';
22
22
  import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
@@ -63,8 +63,8 @@ export class CoreAuthService {
63
63
  protected readonly userService: CoreAuthUserService,
64
64
  protected readonly jwtService: JwtService,
65
65
  protected readonly configService: ConfigService,
66
- @Optional() protected readonly betterAuthService?: BetterAuthService,
67
- @Optional() protected readonly betterAuthUserMapper?: BetterAuthUserMapper,
66
+ @Optional() protected readonly betterAuthService?: CoreBetterAuthService,
67
+ @Optional() protected readonly betterAuthUserMapper?: CoreBetterAuthUserMapper,
68
68
  ) {}
69
69
 
70
70
  /**
@@ -0,0 +1,102 @@
1
+ # Architecture: Why Custom Controllers?
2
+
3
+ The `CoreBetterAuthController` implements custom endpoints instead of directly using native Better-Auth endpoints. This is **necessary** for the nest-server hybrid auth system.
4
+
5
+ ## 1. Hybrid-Auth-System (Legacy + Better-Auth)
6
+
7
+ The nest-server supports bidirectional authentication:
8
+ - **Legacy Auth → Better-Auth**: Users created via Legacy Auth can sign in via Better-Auth
9
+ - **Better-Auth → Legacy Auth**: Users created via Better-Auth can sign in via Legacy Auth
10
+
11
+ This requires custom logic that cannot be implemented via Better-Auth hooks alone.
12
+
13
+ ## 2. Why Not Better-Auth Hooks?
14
+
15
+ Better-Auth hooks have fundamental limitations that prevent full implementation of our requirements:
16
+
17
+ | Requirement | Hook Support | Reason |
18
+ |-------------|--------------|--------|
19
+ | Legacy user migration | ⚠️ Partial | Requires global DB access outside NestJS DI |
20
+ | Password sync to Legacy | ❌ No | **After-hooks don't have access to plaintext password** |
21
+ | Custom response format | ❌ No | **Hooks cannot modify HTTP response** |
22
+ | Multi-cookie setting | ❌ No | **Hooks cannot set cookies** |
23
+ | User mapping with roles | ❌ No | Requires NestJS Dependency Injection |
24
+ | Session token injection | ❌ No | Before-hooks cannot inject tokens into requests |
25
+
26
+ ## 3. Hook Limitations Explained
27
+
28
+ ### After-Hooks Cannot Change Response
29
+
30
+ ```typescript
31
+ // ❌ This does NOT work - return value is ignored
32
+ hooks: {
33
+ after: createAuthMiddleware(async (ctx) => {
34
+ ctx.response.body.customField = 'value'; // Ignored!
35
+ return { response: modifiedResponse }; // Also ignored!
36
+ }),
37
+ }
38
+ ```
39
+
40
+ ### After-Hooks Don't Have Plaintext Password
41
+
42
+ ```typescript
43
+ // ❌ Cannot sync password because it's already hashed
44
+ hooks: {
45
+ after: [
46
+ {
47
+ matcher: (ctx) => ctx.path === '/sign-up/email',
48
+ handler: async (ctx) => {
49
+ // ctx.body.password is ALREADY HASHED at this point
50
+ // We cannot call syncPasswordToLegacy() without plaintext!
51
+ },
52
+ },
53
+ ],
54
+ }
55
+ ```
56
+
57
+ ### Hooks Don't Have NestJS DI Access
58
+
59
+ ```typescript
60
+ // ❌ Hooks are configured in betterAuth(), not in NestJS context
61
+ export const auth = betterAuth({
62
+ hooks: {
63
+ // No access to NestJS services here!
64
+ // this.userService, this.emailService, etc. are unavailable
65
+ },
66
+ });
67
+ ```
68
+
69
+ ## 4. What Custom Endpoints Do
70
+
71
+ | Endpoint | Custom Logic | Why Required |
72
+ |----------|--------------|--------------|
73
+ | `/sign-in/email` | Legacy migration, PW normalization, 2FA handling | Migration needs plaintext password |
74
+ | `/sign-up/email` | PW normalization, Legacy sync, User linking | Sync needs plaintext password |
75
+ | `/sign-out` | Multi-cookie clearing | Response modification |
76
+ | `/session` | User mapping with roles | NestJS service access |
77
+ | Plugin routes | Session token injection | Request modification |
78
+
79
+ ## 5. Native Handler Where Possible
80
+
81
+ Despite custom endpoints, we use Better-Auth's native handler where appropriate:
82
+ - **Plugin routes** (Passkey, 2FA, OAuth) → `authInstance.handler()`
83
+ - **2FA verification flow** → Native handler for correct cookie setting
84
+ - **Passkey authentication** → Native WebAuthn handling
85
+
86
+ ## 6. Alternative Approaches Considered
87
+
88
+ | Approach | Evaluation |
89
+ |----------|------------|
90
+ | **Full Hook Approach** | ❌ Not feasible - missing plaintext password, no response modification |
91
+ | **Hybrid with Global DB** | ⚠️ Possible but anti-pattern - bypasses NestJS DI, harder to test |
92
+ | **Custom Controller (current)** | ✅ Best balance - NestJS DI access, testable, maintainable |
93
+
94
+ ## Conclusion
95
+
96
+ The custom controller architecture is **necessary complexity**, not unnecessary overhead. It enables:
97
+ - ✅ Legacy Auth compatibility
98
+ - ✅ Bidirectional password synchronization
99
+ - ✅ Multi-cookie support
100
+ - ✅ Custom user mapping with roles
101
+ - ✅ Proper 2FA cookie handling
102
+ - ✅ Full NestJS Dependency Injection access
@@ -68,12 +68,12 @@ GraphQL schema is built from decorators at compile time. The parent class (`Core
68
68
 
69
69
  1. Add import:
70
70
  ```typescript
71
- import { BetterAuthUserMapper } from '@lenne.tech/nest-server';
71
+ import { CoreBetterAuthUserMapper } from '@lenne.tech/nest-server';
72
72
  ```
73
73
 
74
74
  2. Add constructor parameter:
75
75
  ```typescript
76
- @Optional() private readonly betterAuthUserMapper?: BetterAuthUserMapper,
76
+ @Optional() private readonly betterAuthUserMapper?: CoreBetterAuthUserMapper,
77
77
  ```
78
78
 
79
79
  3. Pass to super() via options object:
@@ -82,7 +82,7 @@ GraphQL schema is built from decorators at compile time. The parent class (`Core
82
82
  ```
83
83
 
84
84
  **WHY is this critical?**
85
- The `BetterAuthUserMapper` enables bidirectional password synchronization:
85
+ The `CoreBetterAuthUserMapper` enables bidirectional password synchronization:
86
86
  - User signs up via BetterAuth → password synced to Legacy Auth (bcrypt hash)
87
87
  - User changes password → synced between both systems
88
88
  - **Without this, users can only authenticate via ONE system!**
@@ -189,7 +189,7 @@ After integration, verify:
189
189
  | Mistake | Symptom | Fix |
190
190
  |---------|---------|-----|
191
191
  | Forgot to re-declare decorators in Resolver | GraphQL endpoints missing (404) | Copy resolver from reference, keep ALL decorators |
192
- | Forgot `BetterAuthUserMapper` in UserService | Auth systems not synced, users can't cross-authenticate | Add `@Optional()` parameter and pass to super() |
192
+ | Forgot `CoreBetterAuthUserMapper` in UserService | Auth systems not synced, users can't cross-authenticate | Add `@Optional()` parameter and pass to super() |
193
193
  | Missing `fallbackSecrets` in ServerModule | Session issues without explicit secret | Add `fallbackSecrets: [envConfig.jwt?.secret, ...]` |
194
194
  | Wrong `basePath` in config | 404 on BetterAuth endpoints | Ensure basePath matches controller (default: `/iam`) |
195
195
  | Using wrong CoreModule signature | Build errors or missing features | New projects: 1-parameter, Existing: 3-parameter |
@@ -223,29 +223,298 @@ Clients must be configured to use the correct base path and hash passwords:
223
223
 
224
224
  ```typescript
225
225
  // auth-client.ts (e.g., for Nuxt/Vue)
226
+ import { passkeyClient } from '@better-auth/passkey/client';
227
+ import { twoFactorClient } from 'better-auth/client/plugins';
226
228
  import { createAuthClient } from 'better-auth/vue';
227
229
  import { sha256 } from '~/utils/crypto';
228
230
 
229
231
  const baseClient = createAuthClient({
230
232
  baseURL: import.meta.env.VITE_API_URL,
231
233
  basePath: '/iam', // Must match server config
232
- plugins: [...],
234
+ fetchOptions: {
235
+ credentials: 'include', // Required for cross-origin cookie handling
236
+ },
237
+ plugins: [
238
+ twoFactorClient({
239
+ onTwoFactorRedirect() {
240
+ navigateTo('/auth/2fa');
241
+ },
242
+ }),
243
+ passkeyClient(),
244
+ ],
233
245
  });
234
246
 
235
247
  // Wrap signIn/signUp to hash passwords before sending
236
248
  export const authClient = {
237
249
  ...baseClient,
250
+ useSession: baseClient.useSession,
251
+ passkey: (baseClient as any).passkey,
238
252
  signIn: {
239
253
  ...baseClient.signIn,
240
- email: async (params) => {
254
+ email: async (params, options?) => {
255
+ const hashedPassword = await sha256(params.password);
256
+ return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
257
+ },
258
+ // Passkey sign-in (pass through to plugin)
259
+ passkey: (baseClient.signIn as any).passkey,
260
+ },
261
+ signUp: {
262
+ ...baseClient.signUp,
263
+ email: async (params, options?) => {
264
+ const hashedPassword = await sha256(params.password);
265
+ return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
266
+ },
267
+ },
268
+ twoFactor: {
269
+ ...(baseClient as any).twoFactor,
270
+ enable: async (params, options?) => {
271
+ const hashedPassword = await sha256(params.password);
272
+ return (baseClient as any).twoFactor.enable({ password: hashedPassword }, options);
273
+ },
274
+ disable: async (params, options?) => {
241
275
  const hashedPassword = await sha256(params.password);
242
- return baseClient.signIn.email({ ...params, password: hashedPassword });
276
+ return (baseClient as any).twoFactor.disable({ password: hashedPassword }, options);
243
277
  },
278
+ verifyTotp: (baseClient as any).twoFactor.verifyTotp,
279
+ verifyBackupCode: (baseClient as any).twoFactor.verifyBackupCode,
244
280
  },
245
- // ... same for signUp, resetPassword, etc.
246
281
  };
247
282
  ```
248
283
 
284
+ ### 2FA Login Flow (Client-Side)
285
+
286
+ Handle the `twoFactorRedirect` response when signing in:
287
+
288
+ ```typescript
289
+ // login.vue - Handle 2FA redirect
290
+ async function onSubmit(email: string, password: string) {
291
+ const result = await authClient.signIn.email({ email, password });
292
+
293
+ // Check for error
294
+ if (result.error) {
295
+ showError(result.error.message);
296
+ return;
297
+ }
298
+
299
+ // Check if 2FA is required
300
+ if (result.data?.twoFactorRedirect) {
301
+ // User has 2FA enabled - redirect to verification page
302
+ await navigateTo('/auth/2fa');
303
+ return;
304
+ }
305
+
306
+ // Login successful
307
+ if (result.data?.user) {
308
+ setUser(result.data.user);
309
+ await navigateTo('/app');
310
+ }
311
+ }
312
+ ```
313
+
314
+ ```typescript
315
+ // 2fa.vue - Verify TOTP code
316
+ async function verifyCode(code: string) {
317
+ const result = await authClient.twoFactor.verifyTotp({
318
+ code,
319
+ trustDevice: trustDeviceCheckbox.value,
320
+ });
321
+
322
+ if (result.error) {
323
+ showError(result.error.message || 'Invalid code');
324
+ return;
325
+ }
326
+
327
+ // 2FA verification successful
328
+ const userData = result.data?.user;
329
+ if (userData) {
330
+ setUser(userData);
331
+ } else {
332
+ // Fallback: validate session to get user data
333
+ await validateSession();
334
+ }
335
+
336
+ await navigateTo('/app');
337
+ }
338
+
339
+ // Alternative: Use backup code
340
+ async function useBackupCode(code: string) {
341
+ const result = await authClient.twoFactor.verifyBackupCode({ code });
342
+ // Handle same as verifyTotp...
343
+ }
344
+ ```
345
+
346
+ ### Passkey Login Flow (Client-Side)
347
+
348
+ Handle passkey authentication with session validation fallback:
349
+
350
+ ```typescript
351
+ // login.vue - Passkey login
352
+ async function onPasskeyLogin() {
353
+ try {
354
+ // Use official Better Auth passkey sign-in
355
+ const result = await authClient.signIn.passkey();
356
+
357
+ if (result.error) {
358
+ showError(result.error.message || 'Passkey authentication failed');
359
+ return;
360
+ }
361
+
362
+ // Update auth state with user data if available
363
+ if (result.data?.user) {
364
+ setUser(result.data.user);
365
+ } else if (result.data?.session) {
366
+ // IMPORTANT: Passkey auth returns session without user
367
+ // Fetch user data via session validation
368
+ await validateSession();
369
+ }
370
+
371
+ await navigateTo('/app');
372
+ } catch (err) {
373
+ // Handle WebAuthn-specific errors
374
+ if (err instanceof Error && err.name === 'NotAllowedError') {
375
+ showError('Passkey authentication was cancelled');
376
+ return;
377
+ }
378
+ showError(err instanceof Error ? err.message : 'Passkey login failed');
379
+ }
380
+ }
381
+
382
+ // Helper: Validate session and fetch user data
383
+ async function validateSession() {
384
+ const sessionResult = await authClient.$fetch('/session');
385
+ if (sessionResult.user) {
386
+ setUser(sessionResult.user);
387
+ return true;
388
+ }
389
+ return false;
390
+ }
391
+ ```
392
+
393
+ ### Passkey Registration (Client-Side)
394
+
395
+ Register a new passkey for an authenticated user:
396
+
397
+ ```typescript
398
+ // settings.vue - Register new passkey
399
+ async function registerPasskey() {
400
+ try {
401
+ // This calls generate-register-options → verify-registration
402
+ const result = await authClient.passkey.addPasskey({
403
+ name: 'My Device', // Optional: passkey name
404
+ });
405
+
406
+ if (result.error) {
407
+ showError(result.error.message);
408
+ return;
409
+ }
410
+
411
+ showSuccess('Passkey registered successfully!');
412
+ // Refresh passkey list
413
+ await loadPasskeys();
414
+ } catch (err) {
415
+ if (err instanceof Error && err.name === 'NotAllowedError') {
416
+ showError('Passkey registration was cancelled');
417
+ return;
418
+ }
419
+ showError('Failed to register passkey');
420
+ }
421
+ }
422
+
423
+ // List user's passkeys
424
+ async function loadPasskeys() {
425
+ const result = await authClient.passkey.listUserPasskeys();
426
+ if (!result.error) {
427
+ passkeys.value = result.data || [];
428
+ }
429
+ }
430
+
431
+ // Delete a passkey
432
+ async function deletePasskey(passkeyId: string) {
433
+ const result = await authClient.passkey.deletePasskey({ id: passkeyId });
434
+ if (!result.error) {
435
+ await loadPasskeys();
436
+ }
437
+ }
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Better-Auth Hooks: Limitations & Warnings
443
+
444
+ ### Why nest-server Uses Custom Controllers
445
+
446
+ nest-server implements custom REST endpoints instead of relying solely on Better-Auth hooks. This is **by design** due to fundamental hook limitations.
447
+
448
+ ### Hook Limitations Summary
449
+
450
+ | Limitation | Impact |
451
+ |------------|--------|
452
+ | **After-hooks cannot access plaintext password** | Cannot sync password to Legacy Auth after sign-up |
453
+ | **Hooks cannot modify HTTP response** | Cannot customize response format or add custom fields |
454
+ | **Hooks cannot set cookies** | Cannot implement multi-cookie auth strategy |
455
+ | **No NestJS Dependency Injection** | Cannot access services like UserService, EmailService |
456
+ | **Before-hooks cannot inject tokens** | Cannot add session tokens to request headers |
457
+
458
+ ### What You CAN Do with Hooks
459
+
460
+ Better-Auth hooks are suitable for:
461
+ - ✅ Logging and analytics (side effects only)
462
+ - ✅ Sending notifications after events
463
+ - ✅ Simple validation in before-hooks
464
+ - ✅ Database writes using global connection (not recommended)
465
+
466
+ ### What You CANNOT Do with Hooks
467
+
468
+ Do NOT try to implement these via hooks:
469
+ - ❌ Password synchronization between auth systems
470
+ - ❌ Custom response formats
471
+ - ❌ Setting authentication cookies
472
+ - ❌ User role mapping
473
+ - ❌ Legacy auth migration
474
+
475
+ ### Recommended Approach
476
+
477
+ If you need custom authentication logic:
478
+
479
+ 1. **Extend the Controller** - Override methods in `BetterAuthController`
480
+ 2. **Use NestJS Services** - Inject services via constructor
481
+ 3. **Call super()** - Reuse base implementation where possible
482
+
483
+ ```typescript
484
+ // Correct: Custom logic via controller extension
485
+ @Controller('iam')
486
+ export class BetterAuthController extends CoreBetterAuthController {
487
+ constructor(
488
+ betterAuthService: CoreBetterAuthService,
489
+ userMapper: CoreBetterAuthUserMapper,
490
+ configService: ConfigService,
491
+ private readonly analyticsService: AnalyticsService, // Custom service
492
+ ) {
493
+ super(betterAuthService, userMapper, configService);
494
+ }
495
+
496
+ @Post('sign-up/email')
497
+ @Roles(RoleEnum.S_EVERYONE)
498
+ override async signUp(
499
+ @Res({ passthrough: true }) res: Response,
500
+ @Body() input: CoreBetterAuthSignUpInput,
501
+ ): Promise<CoreBetterAuthResponse> {
502
+ const result = await super.signUp(res, input);
503
+
504
+ // Custom logic with full NestJS DI access
505
+ if (result.success) {
506
+ await this.analyticsService.trackSignUp(result.user.id);
507
+ }
508
+
509
+ return result;
510
+ }
511
+ }
512
+ ```
513
+
514
+ ### Further Reading
515
+
516
+ See README.md section "Architecture: Why Custom Controllers?" for detailed explanation.
517
+
249
518
  ---
250
519
 
251
520
  ## Detailed Documentation