@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.
- package/dist/config.env.js +2 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +6 -0
- package/dist/core/common/helpers/logging.helper.js +55 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +37 -19
- package/dist/core/modules/auth/guards/roles.guard.js +33 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/auth/services/core-auth.service.d.ts +5 -5
- package/dist/core/modules/auth/services/core-auth.service.js +4 -4
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +32 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +16 -16
- package/dist/core/modules/better-auth/better-auth.resolver.js +34 -34
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -1
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +10 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +91 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +9 -0
- package/dist/core/modules/better-auth/{better-auth-auth.model.js → core-better-auth-auth.model.js} +17 -17
- package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-migration-status.model.d.ts → core-better-auth-migration-status.model.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-migration-status.model.js → core-better-auth-migration-status.model.js} +14 -14
- package/dist/core/modules/better-auth/core-better-auth-migration-status.model.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-models.d.ts → core-better-auth-models.d.ts} +8 -8
- package/dist/core/modules/better-auth/{better-auth-models.js → core-better-auth-models.js} +61 -61
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +12 -0
- package/dist/core/modules/better-auth/{better-auth-rate-limit.middleware.js → core-better-auth-rate-limit.middleware.js} +10 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.d.ts → core-better-auth-rate-limiter.service.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.js → core-better-auth-rate-limiter.service.js} +8 -8
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-user.mapper.d.ts → core-better-auth-user.mapper.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-user.mapper.js → core-better-auth-user.mapper.js} +10 -9
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +19 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +152 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +23 -32
- package/dist/core/modules/better-auth/core-better-auth.controller.js +184 -201
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +22 -0
- package/dist/core/modules/better-auth/{better-auth.middleware.js → core-better-auth.middleware.js} +45 -18
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth.module.d.ts → core-better-auth.module.d.ts} +6 -6
- package/dist/core/modules/better-auth/{better-auth.module.js → core-better-auth.module.js} +65 -60
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +19 -19
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +18 -18
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/{better-auth.service.d.ts → core-better-auth.service.d.ts} +3 -2
- package/dist/core/modules/better-auth/{better-auth.service.js → core-better-auth.service.js} +75 -35
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +11 -9
- package/dist/core/modules/better-auth/index.js +11 -9
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -1
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +2 -2
- package/dist/core.module.js +6 -6
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +5 -5
- package/dist/server/modules/better-auth/better-auth.controller.js +4 -4
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +3 -3
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +17 -17
- package/dist/server/modules/better-auth/better-auth.resolver.js +18 -18
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +2 -2
- package/dist/server/modules/user/user.service.js +2 -2
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +5 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/src/config.env.ts +15 -0
- package/src/core/common/helpers/logging.helper.ts +134 -0
- package/src/core/common/interfaces/server-options.interface.ts +419 -234
- package/src/core/modules/auth/guards/roles.guard.ts +44 -3
- package/src/core/modules/auth/services/core-auth.service.ts +4 -4
- package/src/core/modules/better-auth/ARCHITECTURE.md +102 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +277 -8
- package/src/core/modules/better-auth/README.md +97 -53
- package/src/core/modules/better-auth/better-auth.config.ts +66 -18
- package/src/core/modules/better-auth/better-auth.resolver.ts +32 -32
- package/src/core/modules/better-auth/better-auth.types.ts +3 -2
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +134 -0
- package/src/core/modules/better-auth/{better-auth-auth.model.ts → core-better-auth-auth.model.ts} +6 -6
- package/src/core/modules/better-auth/{better-auth-migration-status.model.ts → core-better-auth-migration-status.model.ts} +1 -1
- package/src/core/modules/better-auth/{better-auth-models.ts → core-better-auth-models.ts} +9 -9
- package/src/core/modules/better-auth/{better-auth-rate-limit.middleware.ts → core-better-auth-rate-limit.middleware.ts} +5 -5
- package/src/core/modules/better-auth/{better-auth-rate-limiter.service.ts → core-better-auth-rate-limiter.service.ts} +2 -2
- package/src/core/modules/better-auth/{better-auth-user.mapper.ts → core-better-auth-user.mapper.ts} +4 -3
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +272 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +386 -230
- package/src/core/modules/better-auth/{better-auth.middleware.ts → core-better-auth.middleware.ts} +57 -17
- package/src/core/modules/better-auth/{better-auth.module.ts → core-better-auth.module.ts} +77 -66
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +42 -42
- package/src/core/modules/better-auth/{better-auth.service.ts → core-better-auth.service.ts} +86 -40
- package/src/core/modules/better-auth/index.ts +18 -11
- package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +4 -1
- package/src/core/modules/error-code/core-error-code.controller.ts +3 -2
- package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +3 -3
- package/src/core.module.ts +12 -12
- package/src/index.ts +1 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +4 -4
- package/src/server/modules/better-auth/better-auth.module.ts +1 -1
- package/src/server/modules/better-auth/better-auth.resolver.ts +31 -31
- package/src/server/modules/user/user.service.ts +2 -2
- package/src/test/test.helper.ts +13 -1
- package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +0 -9
- package/dist/core/modules/better-auth/better-auth-auth.model.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-models.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +0 -12
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +0 -21
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth.module.js.map +0 -1
- 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 {
|
|
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:
|
|
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(
|
|
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 {
|
|
19
|
-
import {
|
|
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?:
|
|
67
|
-
@Optional() protected readonly 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 {
|
|
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?:
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
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
|