Skip to content

Commit cc06fd7

Browse files
committed
11.7.2: Enable BetterAuth session token authentication via MongoDB lookup with correct collection and ObjectId handling
1 parent 7dea582 commit cc06fd7

30 files changed

+1479
-343
lines changed

.claude/rules/configurable-features.md

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,22 +101,129 @@ auth: {
101101
}
102102
```
103103

104+
## Boolean Shorthand Pattern
105+
106+
For simple enable/disable scenarios, support `boolean | object` configuration:
107+
108+
### Rules
109+
110+
1. **`true`**: Feature is **enabled** with all default values
111+
2. **`false`**: Feature is **disabled**
112+
3. **`{}`**: Feature is **enabled** with all default values (same as `true`)
113+
4. **`{ option: value }`**: Feature is **enabled** with custom settings
114+
5. **`{ enabled: false }`**: Feature is **disabled** (allows pre-configuration)
115+
6. **`undefined`**: Feature is **disabled** (default)
116+
117+
### Benefits
118+
119+
- **Concise**: `jwt: true` instead of `jwt: {}`
120+
- **Readable**: Clear intent at a glance
121+
- **Flexible**: Can still use objects for customization
122+
123+
### Implementation Example
124+
125+
```typescript
126+
// Interface definition
127+
interface IBetterAuth {
128+
jwt?: boolean | IBetterAuthJwtConfig;
129+
twoFactor?: boolean | IBetterAuthTwoFactorConfig;
130+
passkey?: boolean | IBetterAuthPasskeyConfig;
131+
}
132+
133+
interface IBetterAuthJwtConfig {
134+
enabled?: boolean;
135+
expiresIn?: string;
136+
}
137+
138+
// Helper functions
139+
function isPluginEnabled<T extends { enabled?: boolean }>(
140+
config: boolean | T | undefined
141+
): boolean {
142+
if (config === undefined) return false;
143+
if (typeof config === 'boolean') return config;
144+
return config.enabled !== false;
145+
}
146+
147+
function getPluginConfig<T extends { enabled?: boolean }>(
148+
config: boolean | T | undefined
149+
): T | undefined {
150+
if (!isPluginEnabled(config)) return undefined;
151+
if (typeof config === 'boolean') return {} as T;
152+
return config;
153+
}
154+
155+
// Usage in build logic
156+
const jwtConfig = getPluginConfig(config.jwt);
157+
if (jwtConfig) {
158+
plugins.push(jwt({ expirationTime: jwtConfig.expiresIn || '15m' }));
159+
}
160+
```
161+
162+
### Usage Examples
163+
164+
```typescript
165+
// config.env.ts
166+
167+
betterAuth: {
168+
// Boolean shorthand - enable with defaults
169+
jwt: true,
170+
twoFactor: true,
171+
passkey: true,
172+
}
173+
174+
// Equivalent to:
175+
betterAuth: {
176+
jwt: {},
177+
twoFactor: {},
178+
passkey: {},
179+
}
180+
181+
// Mixed - some with defaults, some customized
182+
betterAuth: {
183+
jwt: true, // Enable with defaults
184+
twoFactor: { appName: 'My App' }, // Enable with custom settings
185+
passkey: false, // Explicitly disabled
186+
}
187+
188+
// Pre-configured but disabled
189+
betterAuth: {
190+
jwt: { enabled: false, expiresIn: '1h' }, // Ready to enable later
191+
}
192+
```
193+
104194
## Applied Features
105195

106196
This pattern is currently applied to:
107197

108-
| Feature | Config Path | Default Values |
109-
|---------|-------------|----------------|
110-
| Legacy Auth Rate Limiting | `auth.rateLimit` | `max: 10`, `windowSeconds: 60` |
111-
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | `max: 10`, `windowSeconds: 60` |
198+
| Feature | Config Path | Pattern | Default Values |
199+
|---------|-------------|---------|----------------|
200+
| Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
201+
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
202+
| BetterAuth JWT Plugin | `betterAuth.jwt` | Boolean Shorthand | `expiresIn: '15m'` |
203+
| BetterAuth 2FA Plugin | `betterAuth.twoFactor` | Boolean Shorthand | `appName: 'Nest Server'` |
204+
| BetterAuth Passkey Plugin | `betterAuth.passkey` | Boolean Shorthand | `rpName: 'Nest Server'` |
112205

113206
## Checklist for New Configurable Features
114207

115208
When adding a new configurable feature:
116209

210+
### For "Presence Implies Enabled" Pattern:
211+
117212
- [ ] Define interface with `enabled?: boolean` as optional property
118213
- [ ] Set `enabled: false` in DEFAULT_CONFIG
119214
- [ ] Implement "presence implies enabled" logic in configure method
120215
- [ ] Document all default values in interface JSDoc
121216
- [ ] Add tests for: undefined config, empty object, partial config, explicit disable
217+
218+
### For "Boolean Shorthand" Pattern:
219+
220+
- [ ] Define separate interface for config options (e.g., `IBetterAuthJwtConfig`)
221+
- [ ] Use union type: `property?: boolean | IPropertyConfig`
222+
- [ ] Implement `isPluginEnabled()` helper for boolean/object handling
223+
- [ ] Implement `getPluginConfig()` helper to normalize to object
224+
- [ ] Add tests for: `true`, `false`, `{}`, `{ option: value }`, `{ enabled: false }`, `undefined`
225+
226+
### For Both Patterns:
227+
122228
- [ ] Update this document with the new feature
229+
- [ ] Export new interfaces in `src/index.ts` (if needed)

.claude/rules/role-system.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,46 @@ The `@Roles()` decorator combined with `RolesGuard` automatically:
7272
2. Extracts the user from the token
7373
3. Checks if the user has the required role
7474

75-
**Exception:** `@UseGuards(AuthGuard(...))` is only needed when:
76-
- Using `@Roles(RoleEnum.S_EVERYONE)` but still needing an authenticated context
77-
- Implementing custom authentication strategies outside the role system
75+
### When @UseGuards IS Required
76+
77+
`@UseGuards(AuthGuard(...))` is only needed in these specific cases:
78+
79+
| Case | Example | Reason |
80+
|------|---------|--------|
81+
| **Refresh Token** | `@UseGuards(AuthGuard(AuthGuardStrategy.JWT_REFRESH))` | Different strategy than standard JWT |
82+
| **Custom Strategies** | `@UseGuards(AuthGuard(AuthGuardStrategy.CUSTOM))` | Non-standard authentication flow |
83+
84+
```typescript
85+
// CORRECT: refreshToken needs JWT_REFRESH strategy (not standard JWT)
86+
@Mutation(() => CoreAuthModel)
87+
@Roles(RoleEnum.S_EVERYONE)
88+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT_REFRESH)) // Required - different strategy!
89+
async refreshToken(...): Promise<CoreAuthModel> { }
90+
91+
// CORRECT: Standard endpoints - @Roles is sufficient
92+
@Mutation(() => CoreAuthModel)
93+
@Roles(RoleEnum.S_USER) // Handles JWT auth automatically
94+
async logout(...): Promise<boolean> { }
95+
96+
@Query(() => User)
97+
@Roles(RoleEnum.ADMIN) // Handles JWT auth automatically
98+
async getUser(...): Promise<User> { }
99+
```
100+
101+
### Common Mistake: Redundant Guards
102+
103+
When reviewing code, watch for this anti-pattern:
104+
105+
```typescript
106+
// WRONG: Redundant - @Roles(S_USER) already validates JWT
107+
@Roles(RoleEnum.S_USER)
108+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT)) // DELETE THIS
109+
async someMethod() { }
110+
111+
// WRONG: Redundant - @Roles(ADMIN) already validates JWT
112+
@Roles(RoleEnum.ADMIN)
113+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT)) // DELETE THIS
114+
async adminMethod() { }
115+
```
116+
117+
**Rule of thumb:** If `@Roles()` uses any role OTHER than `S_EVERYONE`, you don't need `@UseGuards(AuthGuard(JWT))`.

.claude/rules/testing.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,41 @@ describe('Feature Name', () => {
7272
});
7373
```
7474

75+
## WebSocket/Subscription Tests
76+
77+
When testing GraphQL subscriptions, use `httpServer.listen(0)` instead of `app.listen()`:
78+
79+
```typescript
80+
// ✅ CORRECT: No startup log, dynamic port
81+
await app.init();
82+
const httpServer = app.getHttpServer();
83+
await new Promise<void>((resolve) => {
84+
httpServer.listen(0, '127.0.0.1', () => resolve());
85+
});
86+
const port = httpServer.address().port;
87+
testHelper = new TestHelper(app, `ws://127.0.0.1:${port}/graphql`);
88+
89+
// Cleanup in afterAll
90+
if (httpServer) {
91+
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
92+
}
93+
await app.close();
94+
```
95+
96+
```typescript
97+
// ❌ WRONG: Produces "Nest application successfully started" log
98+
await app.init();
99+
await app.listen(3030);
100+
```
101+
102+
**Why:**
103+
- `app.listen()` triggers NestJS startup log → noisy test output
104+
- Dynamic port (`0`) avoids port conflicts between parallel tests
105+
- Explicit `httpServer.close()` prevents open handle warnings
106+
75107
## Common Test Issues
76108

77109
- **Tests timeout**: Ensure MongoDB is running
78110
- **Open handles**: Use `npm run test:e2e-doh` to debug
79111
- **Data conflicts**: Use unique identifiers per test
112+
- **"NestApplication successfully started" log**: Use `httpServer.listen()` instead of `app.listen()`

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ See `.claude/rules/versioning.md` for release process.
121121
5. **Use Module Inheritance Pattern** for core modules
122122
6. **Document breaking changes** in commits
123123
7. **Integration Checklists for Core Modules** - Every core module requiring project integration needs `INTEGRATION-CHECKLIST.md` (see `.claude/rules/core-modules.md`)
124+
8. **Don't add redundant @UseGuards** - `@Roles()` already handles JWT auth (see `.claude/rules/role-system.md`)
124125

125126
## Migration Guides
126127

@@ -137,11 +138,11 @@ Detailed documentation in `.claude/rules/`:
137138
| File | Content |
138139
|------|---------|
139140
| `module-inheritance.md` | Core architectural pattern for extending modules |
140-
| `role-system.md` | Role system with S_ prefix rules |
141+
| `role-system.md` | Role system, S_ prefix rules, @Roles vs @UseGuards |
141142
| `architecture.md` | Detailed code architecture |
142143
| `testing.md` | Test configuration and best practices |
143144
| `versioning.md` | Version strategy and release process |
144145
| `core-modules.md` | Path-scoped rules for `src/core/modules/` incl. Integration Checklist requirements |
145146
| `module-deprecation.md` | Legacy Auth → BetterAuth migration roadmap |
146147
| `migration-guides.md` | Process for creating version migration guides |
147-
| `configurable-features.md` | "Presence implies enabled" pattern for optional features |
148+
| `configurable-features.md` | Configuration patterns: "Presence implies enabled" and "Boolean shorthand" (`true` / `{}`) |

package-lock.json

Lines changed: 63 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lenne.tech/nest-server",
3-
"version": "11.7.1",
3+
"version": "11.7.2",
44
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
55
"keywords": [
66
"node",
@@ -81,6 +81,7 @@
8181
"@as-integrations/express5": "1.1.2",
8282
"@better-auth/passkey": "1.4.8-beta.4",
8383
"@getbrevo/brevo": "3.0.1",
84+
"@lenne.tech/nest-server": "file:lenne.tech-nest-server-11.7.2.tgz",
8485
"@nestjs/apollo": "13.2.3",
8586
"@nestjs/common": "11.1.9",
8687
"@nestjs/core": "11.1.9",

spectaql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ servers:
1111
info:
1212
title: lT Nest Server
1313
description: Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).
14-
version: 11.7.1
14+
version: 11.7.2
1515
contact:
1616
name: lenne.Tech GmbH
1717
url: https://lenne.tech

src/core.module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,17 @@ export class CoreModule implements NestModule {
233233
// Add BetterAuthModule based on mode
234234
// IAM-only mode: Always register BetterAuthModule (required for subscription auth)
235235
// Legacy mode: Only register if autoRegister is explicitly true
236-
if (config.betterAuth?.enabled !== false) {
237-
if (isIamOnlyMode || config.betterAuth?.autoRegister === true) {
236+
// betterAuth can be: boolean | IBetterAuth | undefined
237+
const betterAuthConfig = config.betterAuth;
238+
const isBetterAuthEnabled =
239+
betterAuthConfig === true || (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled !== false);
240+
const isAutoRegister = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === true;
241+
242+
if (isBetterAuthEnabled) {
243+
if (isIamOnlyMode || isAutoRegister) {
238244
imports.push(
239245
BetterAuthModule.forRoot({
240-
config: config.betterAuth || {},
246+
config: betterAuthConfig === true ? {} : betterAuthConfig || {},
241247
// Pass JWT secrets for backwards compatibility fallback
242248
fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
243249
}),

0 commit comments

Comments
 (0)