@lenne.tech/nest-server 11.35.1 → 11.36.0
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/.claude/rules/configurable-features.md +1 -0
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/common/services/brevo.service.d.ts +1 -0
- package/dist/core/common/services/brevo.service.js +10 -2
- package/dist/core/common/services/brevo.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +1 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +19 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.35.1-to-11.36.0.md +168 -0
- package/package.json +21 -21
- package/src/core/common/interfaces/server-options.interface.ts +24 -0
- package/src/core/common/services/brevo.service.ts +34 -6
- package/src/core/modules/better-auth/better-auth.config.ts +4 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +38 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Migration Guide: 11.35.1 → 11.36.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | Sign-in and sign-up now **validate their input**. An address that does not satisfy `IsEmail` is refused with a 400 before Better-Auth sees it — which can lock out an existing account created outside `CoreUserInput` (§2) |
|
|
8
|
+
| **New Features** | `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` — opt-in, ends the sessions that already existed when the user resets their password (§1) |
|
|
9
|
+
| **Bugfixes** | `brevo.exclude` stopped excluding every second recipient (§3). Malformed or absent sign-in input answers 400 instead of 500 (§2) |
|
|
10
|
+
| **Migration Effort** | **Read §2 before upgrading** and run the account audit there. §1 is opt-in and off by default; §3 needs nothing |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm update @lenne.tech/nest-server@11.36.0
|
|
18
|
+
pnpm run build
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run the audit in §2 — it is the only step that can surprise you.
|
|
23
|
+
|
|
24
|
+
**Vendor-mode projects:** all three changes are independent and single-file —
|
|
25
|
+
`modules/better-auth/better-auth.config.ts` (§1),
|
|
26
|
+
`modules/better-auth/core-better-auth.controller.ts` (§2),
|
|
27
|
+
`common/services/brevo.service.ts` (§3). §1 also adds the option to
|
|
28
|
+
`common/interfaces/server-options.interface.ts`; taking the config field without the
|
|
29
|
+
`better-auth.config.ts` half gives you a setting that type-checks and does nothing, so sync those two
|
|
30
|
+
together.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. End existing sessions on a password reset (opt-in)
|
|
35
|
+
|
|
36
|
+
A password reset is what somebody reaches for when they suspect their account was taken over.
|
|
37
|
+
Leaving the older sessions alive defeats the point: the attacker keeps theirs, and the new password
|
|
38
|
+
changes nothing for them.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// config.env.ts
|
|
42
|
+
betterAuth: {
|
|
43
|
+
emailAndPassword: {
|
|
44
|
+
revokeSessionsOnPasswordReset: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Forwarded to Better-Auth's native flag, which honours it on **all three** reset routes — password
|
|
50
|
+
reset, email-OTP and phone-number.
|
|
51
|
+
|
|
52
|
+
**Off by default, and staying that way.** It is a behaviour change for an existing deployment: after
|
|
53
|
+
a reset the user is signed out everywhere, including on the devices they still hold. That is worth
|
|
54
|
+
choosing, not worth inflicting on upgrade.
|
|
55
|
+
|
|
56
|
+
The value is matched with `=== true`, so a truthy-but-not-`true` value arriving as JSON through
|
|
57
|
+
`NSC__*` or `NEST_SERVER_CONFIG` (a hand-written `"true"`, say) does **not** silently enable a
|
|
58
|
+
sign-out-everywhere behaviour.
|
|
59
|
+
|
|
60
|
+
> **Do not route this through `betterAuth.options` instead.** That object is spread *shallowly* over
|
|
61
|
+
> the resolved config — only `advanced` is deep-merged — so an `options.emailAndPassword` does not
|
|
62
|
+
> add a key, it **replaces the whole block**, including the scrypt `password.hash` / `password.verify`
|
|
63
|
+
> pair the framework installs. Every credential in your database would stop verifying, at runtime,
|
|
64
|
+
> with no boot error. This is why the flag is a first-class config field.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. Sign-in and sign-up validate their input — read this before upgrading
|
|
69
|
+
|
|
70
|
+
### What changed
|
|
71
|
+
|
|
72
|
+
`CoreBetterAuthSignInInput` and `CoreBetterAuthSignUpInput` carried `@ApiProperty()` only:
|
|
73
|
+
documentation, no validation. Every line of the handler then reads `input.email`, so a body that was
|
|
74
|
+
malformed or absent produced a `TypeError` and reached the client as a **500**.
|
|
75
|
+
|
|
76
|
+
| Request | Before | After |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `{"email": "not-an-email", "password": "x"}` | `500` | `400`, naming the field |
|
|
79
|
+
| `{}` | `500` | `400`, naming both fields |
|
|
80
|
+
| no body at all | `500` | `400` `Missing input` |
|
|
81
|
+
| valid credentials | unchanged | unchanged |
|
|
82
|
+
|
|
83
|
+
On the most-probed endpoint any deployment has, a 500 tells the caller to retry something that can
|
|
84
|
+
never succeed and files their mistake in the same bucket as a real outage. The legacy sign-in
|
|
85
|
+
answered these cases with a 400; that contract is restored.
|
|
86
|
+
|
|
87
|
+
### The part that can lock someone out
|
|
88
|
+
|
|
89
|
+
`@IsEmail()` is now enforced on **sign-in**, not just on sign-up. An account whose stored address
|
|
90
|
+
does not satisfy it can no longer authenticate — the request is refused before Better-Auth is
|
|
91
|
+
reached.
|
|
92
|
+
|
|
93
|
+
This is unlikely but not impossible. `CoreUserInput.email` has always carried the same `IsEmail()`
|
|
94
|
+
validator, so an account created through the normal API cannot be affected. An account can still
|
|
95
|
+
have been created another way:
|
|
96
|
+
|
|
97
|
+
- a seed script or migration writing directly through Mongoose,
|
|
98
|
+
- Better-Auth's own native sign-up route,
|
|
99
|
+
- an import from another system.
|
|
100
|
+
|
|
101
|
+
The addresses at risk are the ones that look fine to a human and fail `validator.js`: no TLD
|
|
102
|
+
(`admin@localhost`), a dotless domain (`test@test`), or trailing whitespace.
|
|
103
|
+
|
|
104
|
+
**Audit before upgrading.** Use the real validator rather than a hand-written pattern — `IsEmail`
|
|
105
|
+
delegates to `validator.js`, whose rules are far more involved than any regex you would write for
|
|
106
|
+
this, and a check that disagrees with the one actually enforced is worse than none:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// node — run from your project root, where class-validator's `validator` resolves
|
|
110
|
+
const isEmail = require('validator/lib/isEmail');
|
|
111
|
+
const { MongoClient } = require('mongodb');
|
|
112
|
+
|
|
113
|
+
const client = await MongoClient.connect(process.env.MONGODB_URI);
|
|
114
|
+
const users = await client.db().collection('users').find({}, { projection: { email: 1 } }).toArray();
|
|
115
|
+
const locked = users.filter(u => !u.email || !isEmail(u.email));
|
|
116
|
+
|
|
117
|
+
console.log(`${locked.length} account(s) can no longer sign in:`);
|
|
118
|
+
locked.forEach(u => console.log(' ', JSON.stringify(u.email), u._id.toString()));
|
|
119
|
+
await client.close();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Anything this lists needs its address corrected before the upgrade, or those users are refused with a
|
|
123
|
+
validation error rather than an authentication error — and the message will not point at the cause.
|
|
124
|
+
|
|
125
|
+
### If you subclass the controller
|
|
126
|
+
|
|
127
|
+
The validation lives on the DTOs, so an override that keeps the parameter types inherits it. A
|
|
128
|
+
subclass that declares its **own** input class is responsible for its own validators —
|
|
129
|
+
`MapAndValidatePipe` walks the prototype chain child-first and skips a property once a child class
|
|
130
|
+
has validated it.
|
|
131
|
+
|
|
132
|
+
`termsAndPrivacyAccepted` is deliberately the one sign-up field without a presence validator: whether
|
|
133
|
+
consent is required is a policy question answered by `betterAuth.signUpValidation` through
|
|
134
|
+
`CoreBetterAuthSignUpValidatorService`, not by the DTO. It does now carry `@IsBoolean()`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 3. `brevo.exclude` excluded only every second recipient
|
|
139
|
+
|
|
140
|
+
`BrevoService` called `.test()` on the configured `RegExp`. Projects declare that pattern with the
|
|
141
|
+
`g` flag:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
brevo: { exclude: /@(testuser\.com|test\.de)/gi }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`RegExp.prototype.test` advances `lastIndex` on a `g` or `y` pattern, and the config holds a single
|
|
148
|
+
shared instance — so the same address answered `true`, `false`, `true`, … across calls. **Every
|
|
149
|
+
second excluded recipient was sent a real, billable transactional mail.**
|
|
150
|
+
|
|
151
|
+
Found in production: a CI end-to-end run delivered five mails to a `@testuser.com` address that the
|
|
152
|
+
project's own exclude pattern lists, and the provider flagged the runner's IP before anyone noticed
|
|
153
|
+
the guard was only half working.
|
|
154
|
+
|
|
155
|
+
The check now matches against a flagless copy, so each call is independent and the configured
|
|
156
|
+
pattern's `lastIndex` is left untouched — it is shared process-wide, and mutating it leaked into
|
|
157
|
+
every later reader. Both send paths were affected; both are fixed.
|
|
158
|
+
|
|
159
|
+
**Nothing to do.** If you kept the `g` flag off as a workaround, you can stop — it no longer matters
|
|
160
|
+
either way.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Module Documentation
|
|
165
|
+
|
|
166
|
+
- BetterAuth integration: [`src/core/modules/better-auth/INTEGRATION-CHECKLIST.md`](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
|
|
167
|
+
- BetterAuth customization: [`src/core/modules/better-auth/CUSTOMIZATION.md`](../src/core/modules/better-auth/CUSTOMIZATION.md)
|
|
168
|
+
- Configuration patterns: [`.claude/rules/configurable-features.md`](../.claude/rules/configurable-features.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.36.0",
|
|
4
4
|
"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).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -91,21 +91,21 @@
|
|
|
91
91
|
"@apollo/server": "5.5.1",
|
|
92
92
|
"@as-integrations/express5": "1.1.2",
|
|
93
93
|
"@better-auth/passkey": "1.6.26",
|
|
94
|
-
"@getbrevo/brevo": "6.0.
|
|
94
|
+
"@getbrevo/brevo": "6.0.3",
|
|
95
95
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
96
|
-
"@nestjs/apollo": "13.4.
|
|
97
|
-
"@nestjs/common": "11.1
|
|
98
|
-
"@nestjs/core": "11.1
|
|
99
|
-
"@nestjs/graphql": "13.4.
|
|
96
|
+
"@nestjs/apollo": "13.4.5",
|
|
97
|
+
"@nestjs/common": "11.2.1",
|
|
98
|
+
"@nestjs/core": "11.2.1",
|
|
99
|
+
"@nestjs/graphql": "13.4.5",
|
|
100
100
|
"@nestjs/jwt": "11.0.2",
|
|
101
101
|
"@nestjs/mongoose": "11.0.4",
|
|
102
102
|
"@nestjs/passport": "11.0.5",
|
|
103
|
-
"@nestjs/platform-express": "11.1
|
|
103
|
+
"@nestjs/platform-express": "11.2.1",
|
|
104
104
|
"@nestjs/schedule": "6.1.3",
|
|
105
|
-
"@nestjs/swagger": "11.4.
|
|
105
|
+
"@nestjs/swagger": "11.4.7",
|
|
106
106
|
"@nestjs/terminus": "11.1.1",
|
|
107
107
|
"@tus/file-store": "2.1.1",
|
|
108
|
-
"@tus/server": "2.4.
|
|
108
|
+
"@tus/server": "2.4.4",
|
|
109
109
|
"@types/supertest": "7.2.1",
|
|
110
110
|
"bcrypt": "6.0.0",
|
|
111
111
|
"better-auth": "1.6.26",
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
"graphql-subscriptions": "3.0.0",
|
|
123
123
|
"graphql-upload": "15.0.2",
|
|
124
124
|
"graphql-ws": "6.2.1",
|
|
125
|
-
"jose": "6.2.
|
|
125
|
+
"jose": "6.2.9",
|
|
126
126
|
"js-sha256": "1.0.0",
|
|
127
127
|
"json-to-graphql-query": "2.3.0",
|
|
128
128
|
"lodash": "4.18.1",
|
|
129
129
|
"mongodb": "7.5.0",
|
|
130
|
-
"mongoose": "9.9.
|
|
130
|
+
"mongoose": "9.9.3",
|
|
131
131
|
"multer": "2.2.0",
|
|
132
132
|
"node-mailjet": "6.0.11",
|
|
133
133
|
"nodemailer": "9.0.5",
|
|
@@ -166,15 +166,15 @@
|
|
|
166
166
|
}
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
|
-
"@aws-sdk/client-s3": "3.
|
|
170
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
169
|
+
"@aws-sdk/client-s3": "3.1112.0",
|
|
170
|
+
"@aws-sdk/s3-request-presigner": "3.1112.0",
|
|
171
171
|
"@compodoc/compodoc": "2.0.0",
|
|
172
172
|
"@nestjs/cli": "11.0.24",
|
|
173
173
|
"@nestjs/schematics": "11.1.0",
|
|
174
|
-
"@nestjs/testing": "11.1
|
|
174
|
+
"@nestjs/testing": "11.2.1",
|
|
175
175
|
"@swc/cli": "0.8.1",
|
|
176
|
-
"@swc/core": "1.
|
|
177
|
-
"@tus/s3-store": "2.0.
|
|
176
|
+
"@swc/core": "1.16.0",
|
|
177
|
+
"@tus/s3-store": "2.0.6",
|
|
178
178
|
"@types/compression": "1.8.1",
|
|
179
179
|
"@types/cookie-parser": "1.4.10",
|
|
180
180
|
"@types/ejs": "3.1.5",
|
|
@@ -187,22 +187,22 @@
|
|
|
187
187
|
"@vitest/coverage-v8": "4.1.10",
|
|
188
188
|
"@vitest/ui": "4.1.10",
|
|
189
189
|
"ansi-colors": "4.1.3",
|
|
190
|
-
"bullmq": "6.
|
|
190
|
+
"bullmq": "6.1.2",
|
|
191
191
|
"find-file-up": "2.0.1",
|
|
192
192
|
"husky": "9.1.7",
|
|
193
193
|
"ioredis": "6.0.0",
|
|
194
194
|
"nodemon": "3.1.14",
|
|
195
195
|
"npm-watch": "0.13.0",
|
|
196
196
|
"otpauth": "9.5.1",
|
|
197
|
-
"oxfmt": "0.
|
|
198
|
-
"oxlint": "1.
|
|
197
|
+
"oxfmt": "0.63.0",
|
|
198
|
+
"oxlint": "1.78.0",
|
|
199
199
|
"rimraf": "6.1.3",
|
|
200
200
|
"ts-node": "10.9.2",
|
|
201
201
|
"tsconfig-paths": "4.2.0",
|
|
202
|
-
"tsx": "4.23.
|
|
202
|
+
"tsx": "4.23.12",
|
|
203
203
|
"tus-js-client": "4.3.1",
|
|
204
204
|
"typescript": "5.9.3",
|
|
205
|
-
"unplugin-swc": "1.5.
|
|
205
|
+
"unplugin-swc": "1.5.11",
|
|
206
206
|
"vite": "8.2.1",
|
|
207
207
|
"vite-plugin-node": "8.0.0",
|
|
208
208
|
"vitest": "4.1.10"
|
|
@@ -3414,6 +3414,30 @@ interface IBetterAuthBase {
|
|
|
3414
3414
|
* @default true
|
|
3415
3415
|
*/
|
|
3416
3416
|
enabled?: boolean;
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* End every existing session when the user completes a password reset.
|
|
3420
|
+
*
|
|
3421
|
+
* A reset is what somebody reaches for when they suspect their account was
|
|
3422
|
+
* taken over, so leaving the older sessions alive defeats the point: the
|
|
3423
|
+
* attacker keeps theirs and the new password changes nothing for them.
|
|
3424
|
+
*
|
|
3425
|
+
* Left off by default because it is a behaviour change for existing
|
|
3426
|
+
* deployments — a reset then signs the user out everywhere, including on
|
|
3427
|
+
* the devices they still hold.
|
|
3428
|
+
*
|
|
3429
|
+
* Passed through to better-auth's native
|
|
3430
|
+
* `emailAndPassword.revokeSessionsOnPasswordReset`. It cannot be set via
|
|
3431
|
+
* `options` instead: that object is spread SHALLOWLY over the resolved
|
|
3432
|
+
* config, so an `options.emailAndPassword` would replace the whole block
|
|
3433
|
+
* — including the scrypt `password.hash` / `password.verify` pair this
|
|
3434
|
+
* framework installs — and every credential in the database would stop
|
|
3435
|
+
* verifying.
|
|
3436
|
+
*
|
|
3437
|
+
* @default false
|
|
3438
|
+
* @since 11.36.0
|
|
3439
|
+
*/
|
|
3440
|
+
revokeSessionsOnPasswordReset?: boolean;
|
|
3417
3441
|
};
|
|
3418
3442
|
|
|
3419
3443
|
/**
|
|
@@ -53,9 +53,8 @@ export class BrevoService {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Exclude (test) users
|
|
57
|
-
|
|
58
|
-
if (this.configService.config?.brevo?.exclude?.test?.(to)) {
|
|
56
|
+
// Exclude (test) users
|
|
57
|
+
if (this.isExcluded(to)) {
|
|
59
58
|
return 'TEST_USER!';
|
|
60
59
|
}
|
|
61
60
|
|
|
@@ -98,9 +97,8 @@ export class BrevoService {
|
|
|
98
97
|
return false;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
// Exclude (test) users
|
|
102
|
-
|
|
103
|
-
if (this.configService.config?.brevo?.exclude?.test?.(to)) {
|
|
100
|
+
// Exclude (test) users
|
|
101
|
+
if (this.isExcluded(to)) {
|
|
104
102
|
return 'TEST_USER!';
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -135,6 +133,36 @@ export class BrevoService {
|
|
|
135
133
|
return { 'Idempotency-Key': randomUUID() };
|
|
136
134
|
}
|
|
137
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Checks a recipient against `brevo.exclude` without inheriting the pattern's match state.
|
|
138
|
+
*
|
|
139
|
+
* Two traps live in this one line, and both have bitten:
|
|
140
|
+
*
|
|
141
|
+
* 1. `RegExp.prototype.test` ADVANCES `lastIndex` on a pattern carrying `g` or `y`. The config
|
|
142
|
+
* holds a single shared instance and projects declare it as `/…/gi`, so calling `.test()` on
|
|
143
|
+
* it directly answers true, false, true, … for the very same address — every second excluded
|
|
144
|
+
* recipient receives a real mail. Matching against a flagless copy keeps each call
|
|
145
|
+
* independent and leaves the configured pattern untouched.
|
|
146
|
+
* 2. It must be read from `config`, never from `configFastButReadOnly`: assigning `lastIndex` on
|
|
147
|
+
* the frozen copy throws `TypeError: Cannot assign to read only property 'lastIndex'`.
|
|
148
|
+
* Point 1 removes the assignment, but the frozen object may still be a `deepFreeze`d clone
|
|
149
|
+
* whose flags differ, so the mutable side stays the source of truth.
|
|
150
|
+
*
|
|
151
|
+
* @param to - Recipient email address
|
|
152
|
+
* @returns `true` when the recipient matches the configured exclude pattern
|
|
153
|
+
*/
|
|
154
|
+
protected isExcluded(to: string): boolean {
|
|
155
|
+
const exclude = this.configService.config?.brevo?.exclude;
|
|
156
|
+
if (typeof exclude?.test !== 'function') {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const stateless =
|
|
161
|
+
exclude.global || exclude.sticky ? new RegExp(exclude.source, exclude.flags.replace(/[gy]/g, '')) : exclude;
|
|
162
|
+
|
|
163
|
+
return stateless.test(to);
|
|
164
|
+
}
|
|
165
|
+
|
|
138
166
|
/**
|
|
139
167
|
* Lazily constructs (and memoises) the Brevo SDK client.
|
|
140
168
|
*
|
|
@@ -444,6 +444,10 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Crea
|
|
|
444
444
|
hash: nativeScryptHash,
|
|
445
445
|
verify: nativeScryptVerify,
|
|
446
446
|
},
|
|
447
|
+
// Opt-in: a reset then also ends the sessions that already existed, which
|
|
448
|
+
// is the point of resetting after a suspected takeover. Off by default so
|
|
449
|
+
// the behaviour of existing deployments does not change under them.
|
|
450
|
+
revokeSessionsOnPasswordReset: config.emailAndPassword?.revokeSessionsOnPasswordReset === true,
|
|
447
451
|
},
|
|
448
452
|
plugins,
|
|
449
453
|
secret: validation.resolvedSecret || config.secret,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ApiProperty,
|
|
25
25
|
ApiTags,
|
|
26
26
|
} from '@nestjs/swagger';
|
|
27
|
+
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
27
28
|
import { Request, Response } from 'express';
|
|
28
29
|
|
|
29
30
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
@@ -122,10 +123,18 @@ export class CoreBetterAuthResponse {
|
|
|
122
123
|
* Sign-in input DTO
|
|
123
124
|
*/
|
|
124
125
|
export class CoreBetterAuthSignInInput {
|
|
126
|
+
// Validated, not merely documented. Without these the sign-in endpoint — the
|
|
127
|
+
// most-probed surface of any deployment — answered malformed input with a
|
|
128
|
+
// 500 from the first property read, which tells a client to retry something
|
|
129
|
+
// that can never work and buries real faults in the same bucket.
|
|
125
130
|
@ApiProperty({ description: 'User email address', example: 'user@example.com' })
|
|
131
|
+
@IsNotEmpty()
|
|
132
|
+
@IsEmail()
|
|
126
133
|
email: string;
|
|
127
134
|
|
|
128
135
|
@ApiProperty({ description: 'User password' })
|
|
136
|
+
@IsNotEmpty()
|
|
137
|
+
@IsString()
|
|
129
138
|
password: string;
|
|
130
139
|
}
|
|
131
140
|
|
|
@@ -134,15 +143,30 @@ export class CoreBetterAuthSignInInput {
|
|
|
134
143
|
*/
|
|
135
144
|
export class CoreBetterAuthSignUpInput {
|
|
136
145
|
@ApiProperty({ description: 'User email address', example: 'user@example.com' })
|
|
146
|
+
@IsNotEmpty()
|
|
147
|
+
@IsEmail()
|
|
137
148
|
email: string;
|
|
138
149
|
|
|
139
150
|
@ApiProperty({ description: 'Display name', example: 'John Doe', required: false })
|
|
151
|
+
@IsOptional()
|
|
152
|
+
@IsString()
|
|
140
153
|
name?: string;
|
|
141
154
|
|
|
142
155
|
@ApiProperty({ description: 'User password (min 8 characters)' })
|
|
156
|
+
@IsNotEmpty()
|
|
157
|
+
@IsString()
|
|
143
158
|
password: string;
|
|
144
159
|
|
|
160
|
+
// Deliberately left without a validator, unlike its three neighbours. Whether this field is
|
|
161
|
+
// REQUIRED is a policy question the framework cannot answer here — it depends on
|
|
162
|
+
// `betterAuth.signUpValidation`, which the handler consults through
|
|
163
|
+
// CoreBetterAuthSignUpValidatorService before anything else happens. A `@IsNotEmpty()` here would
|
|
164
|
+
// hard-code "consent is mandatory" into the DTO and reject a sign-up for a deployment that never
|
|
165
|
+
// asked for consent, with a message that names the wrong cause. The others carry validators
|
|
166
|
+
// because "an email must look like an email" needs no policy.
|
|
145
167
|
@ApiProperty({ description: 'Whether user accepted terms and privacy policy', required: false })
|
|
168
|
+
@IsBoolean()
|
|
169
|
+
@IsOptional()
|
|
146
170
|
termsAndPrivacyAccepted?: boolean;
|
|
147
171
|
}
|
|
148
172
|
|
|
@@ -329,6 +353,15 @@ export class CoreBetterAuthController {
|
|
|
329
353
|
): Promise<CoreBetterAuthResponse> {
|
|
330
354
|
this.ensureEnabled();
|
|
331
355
|
|
|
356
|
+
// A request with no body at all reaches here as `undefined`, and every line
|
|
357
|
+
// below reads `input.email`. Without this the caller gets a 500 for what is
|
|
358
|
+
// plainly their own malformed request — and a 500 tells a client to retry
|
|
359
|
+
// later, which will never help. The legacy sign-in answered this case with
|
|
360
|
+
// a 400 "Missing input"; keep that contract.
|
|
361
|
+
if (!input) {
|
|
362
|
+
throw new BadRequestException('Missing input');
|
|
363
|
+
}
|
|
364
|
+
|
|
332
365
|
const api = this.betterAuthService.getApi();
|
|
333
366
|
if (!api) {
|
|
334
367
|
throw new BadRequestException(ErrorCode.BETTERAUTH_API_NOT_AVAILABLE);
|
|
@@ -494,6 +527,11 @@ export class CoreBetterAuthController {
|
|
|
494
527
|
this.ensureEnabled();
|
|
495
528
|
this.betterAuthService.ensureSignUpEnabled();
|
|
496
529
|
|
|
530
|
+
// Same reasoning as in signIn: a body-less request must not become a 500.
|
|
531
|
+
if (!input) {
|
|
532
|
+
throw new BadRequestException('Missing input');
|
|
533
|
+
}
|
|
534
|
+
|
|
497
535
|
// Validate sign-up input (termsAndPrivacyAccepted is required by default)
|
|
498
536
|
if (this.signUpValidator) {
|
|
499
537
|
this.signUpValidator.validateSignUpInput({ termsAndPrivacyAccepted: input.termsAndPrivacyAccepted });
|