@lenne.tech/nest-server 11.37.0 → 11.39.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 +51 -0
- package/.claude/rules/module-deprecation.md +25 -1
- package/.claude/rules/package-management.md +66 -0
- package/.claude/rules/testing.md +81 -5
- package/FRAMEWORK-API.md +4 -2
- package/dist/config.env.js +5 -3
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/config.helper.d.ts +2 -0
- package/dist/core/common/helpers/config.helper.js +18 -0
- package/dist/core/common/helpers/config.helper.js.map +1 -1
- package/dist/core/common/helpers/cookies.helper.d.ts +3 -0
- package/dist/core/common/helpers/cookies.helper.js +9 -0
- package/dist/core/common/helpers/cookies.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +1 -0
- package/dist/core/common/helpers/input.helper.js +4 -0
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +6 -1
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +1 -0
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +21 -0
- package/dist/core/common/middlewares/security-headers.middleware.d.ts +18 -0
- package/dist/core/common/middlewares/security-headers.middleware.js +90 -0
- package/dist/core/common/middlewares/security-headers.middleware.js.map +1 -0
- package/dist/core/common/services/email.service.d.ts +4 -1
- package/dist/core/common/services/email.service.js +25 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/module.service.js +1 -0
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.controller.js +2 -5
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +2 -0
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +2 -5
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.d.ts +12 -0
- package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.js +83 -0
- package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.js.map +1 -0
- package/dist/core/modules/auth/helpers/legacy-endpoints.helper.d.ts +3 -0
- package/dist/core/modules/auth/helpers/legacy-endpoints.helper.js +14 -0
- package/dist/core/modules/auth/helpers/legacy-endpoints.helper.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +12 -0
- package/dist/core/modules/better-auth/better-auth.config.js +40 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +9 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +53 -4
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +5 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +57 -4
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-error-codes.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-error-codes.helper.js +54 -0
- package/dist/core/modules/better-auth/core-better-auth-error-codes.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.js +19 -0
- package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +4 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +13 -10
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth.module.js +34 -4
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +27 -0
- package/dist/core/modules/error-code/error-codes.js +24 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/hub/core-hub.service.js +1 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -1
- package/dist/core/modules/user/core-user.model.d.ts +1 -0
- package/dist/core/modules/user/core-user.model.js +11 -1
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +10 -1
- package/dist/core/modules/user/core-user.service.js +124 -15
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/inputs/core-user.input.js +1 -1
- package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
- package/dist/core.module.js +2 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +3 -0
- package/dist/server/modules/user/user.controller.js +2 -1
- package/dist/server/modules/user/user.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.d.ts +5 -0
- package/dist/server/modules/user/user.resolver.js +2 -1
- package/dist/server/modules/user/user.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +18 -5
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/templates/password-reset-de.ejs +12 -0
- package/dist/templates/password-reset-en.ejs +12 -0
- package/dist/templates/password-reset.ejs +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +49 -1
- package/migration-guides/11.37.x-to-11.38.x.md +528 -0
- package/migration-guides/11.38.x-to-11.39.x.md +456 -0
- package/package.json +1 -1
- package/src/config.env.ts +28 -4
- package/src/core/common/helpers/config.helper.ts +79 -0
- package/src/core/common/helpers/cookies.helper.ts +38 -0
- package/src/core/common/helpers/input.helper.ts +37 -0
- package/src/core/common/helpers/service.helper.ts +9 -1
- package/src/core/common/interceptors/check-security.interceptor.ts +1 -0
- package/src/core/common/interfaces/server-options.interface.ts +326 -10
- package/src/core/common/middlewares/security-headers.middleware.ts +155 -0
- package/src/core/common/services/email.service.ts +46 -1
- package/src/core/common/services/module.service.ts +1 -0
- package/src/core/modules/auth/README.md +104 -0
- package/src/core/modules/auth/core-auth.controller.ts +4 -11
- package/src/core/modules/auth/core-auth.module.ts +5 -0
- package/src/core/modules/auth/core-auth.resolver.ts +4 -11
- package/src/core/modules/auth/core-legacy-auth-deprecation.initializer.ts +128 -0
- package/src/core/modules/auth/helpers/legacy-endpoints.helper.ts +69 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +22 -0
- package/src/core/modules/better-auth/README.md +32 -59
- package/src/core/modules/better-auth/better-auth.config.ts +131 -4
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +126 -3
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +166 -7
- package/src/core/modules/better-auth/core-better-auth-error-codes.helper.ts +146 -0
- package/src/core/modules/better-auth/core-better-auth-password-reset.registry.ts +92 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +42 -10
- package/src/core/modules/better-auth/core-better-auth.module.ts +67 -4
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/error-code/error-codes.ts +55 -0
- package/src/core/modules/hub/core-hub.service.ts +1 -0
- package/src/core/modules/user/core-user.model.ts +28 -1
- package/src/core/modules/user/core-user.service.ts +389 -22
- package/src/core/modules/user/inputs/core-user.input.ts +16 -1
- package/src/core.module.ts +8 -0
- package/src/index.ts +3 -0
- package/src/server/modules/user/user.controller.ts +7 -1
- package/src/server/modules/user/user.resolver.ts +7 -1
- package/src/server/modules/user/user.service.ts +58 -13
- package/src/templates/password-reset-de.ejs +12 -0
- package/src/templates/password-reset-en.ejs +12 -0
- package/src/templates/password-reset.ejs +1 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { Inject, Injectable, OnModuleDestroy, Optional } from '@nestjs/common';
|
|
2
|
+
import { Inject, Injectable, Logger, OnModuleDestroy, Optional } from '@nestjs/common';
|
|
3
3
|
import nodemailer = require('nodemailer');
|
|
4
4
|
import { Attachment } from 'nodemailer/lib/mailer';
|
|
5
5
|
|
|
6
|
+
import { isImpossibleSmtpTlsCombination } from '../helpers/config.helper';
|
|
6
7
|
import { isNonEmptyString, isTrue, returnFalse } from '../helpers/input.helper';
|
|
7
8
|
import { MailTransportOptions } from '../interfaces/server-options.interface';
|
|
8
9
|
import { HUB_EMAIL_CAPTURE } from '../../modules/hub/hub.constants';
|
|
@@ -22,6 +23,11 @@ export class EmailService implements OnModuleDestroy {
|
|
|
22
23
|
private cachedTransporter: nodemailer.Transporter | null = null;
|
|
23
24
|
private cachedSmtpConfig: string | null = null;
|
|
24
25
|
|
|
26
|
+
protected readonly emailServiceLogger = new Logger(EmailService.name);
|
|
27
|
+
|
|
28
|
+
/** Once-per-process latch for the SMTP TLS warning — a per-send warning would be noise. */
|
|
29
|
+
private smtpTlsWarningEmitted = false;
|
|
30
|
+
|
|
25
31
|
/**
|
|
26
32
|
* Inject services
|
|
27
33
|
*/
|
|
@@ -33,6 +39,43 @@ export class EmailService implements OnModuleDestroy {
|
|
|
33
39
|
@Optional() @Inject(HUB_EMAIL_CAPTURE) protected readonly emailCapture?: IHubEmailCapture,
|
|
34
40
|
) {}
|
|
35
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Warn once when the SMTP port and TLS mode describe a connection that cannot be established.
|
|
44
|
+
*
|
|
45
|
+
* `secure: true` means implicit TLS, which only port 465 speaks. On 587 — the submission port,
|
|
46
|
+
* which upgrades through STARTTLS — nodemailer sends a TLS ClientHello, the server answers with
|
|
47
|
+
* an SMTP greeting, and OpenSSL reports `wrong version number`. This package's own `production`
|
|
48
|
+
* profile shipped that pair, and because authentication mail is deliberately not awaited, the
|
|
49
|
+
* API answered 200 while every message died in transport. It was found in production.
|
|
50
|
+
*
|
|
51
|
+
* Reported rather than corrected: a deployment may legitimately run submission on a non-standard
|
|
52
|
+
* port, and silently overriding an explicit setting is how the original defect stayed invisible.
|
|
53
|
+
* A warning names the problem without deciding it.
|
|
54
|
+
*/
|
|
55
|
+
protected warnOnImpossibleSmtpTlsCombination(smtp: unknown): void {
|
|
56
|
+
if (this.smtpTlsWarningEmitted || typeof smtp !== 'object' || smtp === null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { port, secure } = smtp as { port?: unknown; secure?: unknown };
|
|
61
|
+
if (typeof port !== 'number' || typeof secure !== 'boolean') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isImpossibleSmtpTlsCombination(port, secure)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.smtpTlsWarningEmitted = true;
|
|
70
|
+
this.emailServiceLogger.warn(
|
|
71
|
+
`SMTP is configured with port ${port} and secure: ${secure}, a combination that cannot connect. ` +
|
|
72
|
+
'`secure: true` starts TLS immediately, which only port 465 supports; port 587 and friends open in ' +
|
|
73
|
+
'plaintext and upgrade via STARTTLS, which needs `secure: false`. Expect ' +
|
|
74
|
+
'"wrong version number" from OpenSSL and NO outgoing mail — silently, because authentication mail ' +
|
|
75
|
+
'is not awaited. Set SMTP_SECURE=false for port 587, or SMTP_PORT=465 to keep implicit TLS.',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
36
79
|
onModuleDestroy(): void {
|
|
37
80
|
if (this.cachedTransporter) {
|
|
38
81
|
this.cachedTransporter.close();
|
|
@@ -109,6 +152,8 @@ export class EmailService implements OnModuleDestroy {
|
|
|
109
152
|
}
|
|
110
153
|
}
|
|
111
154
|
|
|
155
|
+
this.warnOnImpossibleSmtpTlsCombination(smtp);
|
|
156
|
+
|
|
112
157
|
// Hub mailbox capture (Mailpit-style). Runs after templates are rendered, before the transport.
|
|
113
158
|
// In capture mode it records the mail and suppresses the send (returns a jsonTransport-like ack).
|
|
114
159
|
// Fully guarded: a broken mailbox hook must never break (or crash) the mail path.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Auth Module (Legacy Auth)
|
|
2
|
+
|
|
3
|
+
JWT-based email/password authentication: `signIn`, `signUp`, `logout`, `refreshToken` over GraphQL,
|
|
4
|
+
and `POST /auth/signin`, `/auth/signup`, `/auth/logout`, `/auth/refresh-token` over REST.
|
|
5
|
+
|
|
6
|
+
> **This module is deprecated in favour of IAM (Better-Auth) and will be removed.** Since 11.38.0
|
|
7
|
+
> its endpoints are **off unless explicitly enabled**. New projects should not register it — the
|
|
8
|
+
> one-argument `CoreModule.forRoot(envConfig)` never does. See
|
|
9
|
+
> [`.claude/rules/module-deprecation.md`](../../../../.claude/rules/module-deprecation.md) for the
|
|
10
|
+
> migration path.
|
|
11
|
+
|
|
12
|
+
## When this module exists at all
|
|
13
|
+
|
|
14
|
+
Only when a project passes the three-argument form:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The one-argument form (`CoreModule.forRoot(envConfig)`) registers IAM only, and nothing in this
|
|
21
|
+
directory is loaded. If you are reading this while planning a new project, that is the form you
|
|
22
|
+
want.
|
|
23
|
+
|
|
24
|
+
## Enabling the endpoints
|
|
25
|
+
|
|
26
|
+
Registering the module is no longer enough. Since 11.38.0 the endpoints answer **HTTP 410 Gone**
|
|
27
|
+
until asked for:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// config.env.ts
|
|
31
|
+
auth: {
|
|
32
|
+
legacyEndpoints: { enabled: true }, // or LEGACY_AUTH_ENABLED=true
|
|
33
|
+
},
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The default flipped because a second, fully functional password-authentication surface that nobody
|
|
37
|
+
chose is a liability: it is served by `CoreAuthService` rather than Better-Auth, so it bypasses
|
|
38
|
+
every IAM-side control an operator may believe is now in force — two-factor, passkey enforcement,
|
|
39
|
+
Better-Auth session revocation.
|
|
40
|
+
|
|
41
|
+
Resolution is one exported function, `isLegacyEndpointEnabled(config, transport)`, shared by both
|
|
42
|
+
transports so they cannot answer differently:
|
|
43
|
+
|
|
44
|
+
| Configuration | Result |
|
|
45
|
+
| ------------------------------------------- | ------------------------------------ |
|
|
46
|
+
| `enabled: false` | off, whatever `graphql` / `rest` say |
|
|
47
|
+
| per-transport flag set (`graphql` / `rest`) | that flag wins |
|
|
48
|
+
| `enabled: true` | on |
|
|
49
|
+
| nothing set | **off** |
|
|
50
|
+
|
|
51
|
+
An explicit `enabled: false` is a hard off switch that a per-transport `true` cannot reopen — it is
|
|
52
|
+
the setting a project reached for to close legacy down, and an upgrade must never widen it.
|
|
53
|
+
|
|
54
|
+
## Knowing when you can turn it off
|
|
55
|
+
|
|
56
|
+
`CoreLegacyAuthDeprecationInitializer` reports at every boot, in both directions:
|
|
57
|
+
|
|
58
|
+
- while any endpoint is open: a deprecation warning plus the IAM migration percentage,
|
|
59
|
+
- while all are closed but users are still unmigrated: a warning that those users cannot sign in,
|
|
60
|
+
naming the way back.
|
|
61
|
+
|
|
62
|
+
Both are best-effort — the status read runs detached from the boot so it cannot delay readiness.
|
|
63
|
+
The same figures are available on demand through the `betterAuthMigrationStatus` query
|
|
64
|
+
(`canDisableLegacyAuth` is the signal to act on).
|
|
65
|
+
|
|
66
|
+
## Relationship to IAM while both run
|
|
67
|
+
|
|
68
|
+
The two systems share one user collection and keep each other's credentials in step:
|
|
69
|
+
|
|
70
|
+
| Event | Written by | Mirrored into |
|
|
71
|
+
| ------------------------------------------- | --------------- | ------------------------------------- |
|
|
72
|
+
| Sign-up via IAM | IAM (scrypt) | legacy (bcrypt) |
|
|
73
|
+
| Sign-up via Legacy | legacy (bcrypt) | IAM, on first IAM sign-in (migration) |
|
|
74
|
+
| Password reset via the legacy user endpoint | legacy | IAM |
|
|
75
|
+
| Password reset via IAM | IAM | legacy (11.38.0+) |
|
|
76
|
+
| Password change via `update()` | legacy | IAM |
|
|
77
|
+
|
|
78
|
+
`CoreAuthService.signIn` **delegates to IAM** as soon as a user carries an `iamId`, so for a
|
|
79
|
+
migrated user both endpoints answer from the IAM credential. That is worth knowing when debugging:
|
|
80
|
+
a stale legacy bcrypt hash is invisible through either API and only surfaces if IAM is switched
|
|
81
|
+
off.
|
|
82
|
+
|
|
83
|
+
Hash formats differ irreversibly (`bcrypt(sha256(pw))` vs `scrypt(sha256(pw))`), which is why the
|
|
84
|
+
mirrors exist rather than a one-off migration script.
|
|
85
|
+
|
|
86
|
+
## Extending
|
|
87
|
+
|
|
88
|
+
Standard Module Inheritance Pattern. Both endpoint guards are `protected` and must be called from
|
|
89
|
+
an override:
|
|
90
|
+
|
|
91
|
+
| Class | Method to call |
|
|
92
|
+
| -------------------- | --------------------------------- |
|
|
93
|
+
| `CoreAuthResolver` | `checkLegacyGraphQLEnabled(name)` |
|
|
94
|
+
| `CoreAuthController` | `checkLegacyRESTEnabled(name)` |
|
|
95
|
+
|
|
96
|
+
Forgetting one re-opens a disabled endpoint. Use the exported `isLegacyEndpointEnabled()` rather
|
|
97
|
+
than re-implementing the table if you override the check itself.
|
|
98
|
+
|
|
99
|
+
## Related
|
|
100
|
+
|
|
101
|
+
- [`.claude/rules/module-deprecation.md`](../../../../.claude/rules/module-deprecation.md) — the migration roadmap
|
|
102
|
+
- [`.claude/rules/role-system.md`](../../../../.claude/rules/role-system.md) — `@Roles` vs `@UseGuards`, and why `refreshToken` is the one place `@UseGuards` is still required
|
|
103
|
+
- [`../better-auth/README.md`](../better-auth/README.md) — the system replacing this one
|
|
104
|
+
- [`../better-auth/INTEGRATION-CHECKLIST.md`](../better-auth/INTEGRATION-CHECKLIST.md) — integrating IAM
|
|
@@ -22,6 +22,7 @@ import { CoreAuthModel } from './core-auth.model';
|
|
|
22
22
|
import { LegacyAuthDisabledException } from './exceptions/legacy-auth-disabled.exception';
|
|
23
23
|
import { AuthGuard } from './guards/auth.guard';
|
|
24
24
|
import { LegacyAuthRateLimitGuard } from './guards/legacy-auth-rate-limit.guard';
|
|
25
|
+
import { isLegacyEndpointEnabled } from './helpers/legacy-endpoints.helper';
|
|
25
26
|
import { CoreAuthSignInInput } from './inputs/core-auth-sign-in.input';
|
|
26
27
|
import { CoreAuthSignUpInput } from './inputs/core-auth-sign-up.input';
|
|
27
28
|
import { ICoreAuthUser } from './interfaces/core-auth-user.interface';
|
|
@@ -70,23 +71,15 @@ export class CoreAuthController {
|
|
|
70
71
|
/**
|
|
71
72
|
* Check if legacy REST endpoints are enabled
|
|
72
73
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* - config.auth.legacyEndpoints.rest is false
|
|
74
|
+
* Since 11.38.0 legacy endpoints are OFF unless a project asks for them — see
|
|
75
|
+
* `isLegacyEndpointEnabled()` for the full resolution table.
|
|
76
76
|
*
|
|
77
77
|
* @throws LegacyAuthDisabledException
|
|
78
78
|
*/
|
|
79
79
|
protected checkLegacyRESTEnabled(endpointName: string): void {
|
|
80
80
|
const authConfig = this.configService.getFastButReadOnly('auth');
|
|
81
|
-
const legacyConfig = authConfig?.legacyEndpoints;
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
if (legacyConfig?.enabled === false) {
|
|
85
|
-
throw new LegacyAuthDisabledException(endpointName);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Check if REST endpoints specifically are disabled
|
|
89
|
-
if (legacyConfig?.rest === false) {
|
|
82
|
+
if (!isLegacyEndpointEnabled(authConfig?.legacyEndpoints, 'rest')) {
|
|
90
83
|
throw new LegacyAuthDisabledException(endpointName);
|
|
91
84
|
}
|
|
92
85
|
}
|
|
@@ -12,6 +12,7 @@ import { RolesGuardRegistry } from './guards/roles-guard-registry';
|
|
|
12
12
|
import { RolesGuard } from './guards/roles.guard';
|
|
13
13
|
import { CoreAuthUserService } from './services/core-auth-user.service';
|
|
14
14
|
import { CoreAuthService } from './services/core-auth.service';
|
|
15
|
+
import { CoreLegacyAuthDeprecationInitializer } from './core-legacy-auth-deprecation.initializer';
|
|
15
16
|
import { LegacyAuthRateLimiter } from './services/legacy-auth-rate-limiter.service';
|
|
16
17
|
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';
|
|
17
18
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
|
@@ -64,6 +65,10 @@ export class CoreAuthModule {
|
|
|
64
65
|
let providers: any[] = [
|
|
65
66
|
// [Global] The GraphQLAuthGuard integrates the user into context
|
|
66
67
|
...rolesGuardProvider,
|
|
68
|
+
// Reports at boot that this deployment still exposes Legacy Auth, and how far the
|
|
69
|
+
// IAM migration has come. Registered here because this module exists only in
|
|
70
|
+
// legacy mode — an IAM-only project never loads it.
|
|
71
|
+
CoreLegacyAuthDeprecationInitializer,
|
|
67
72
|
{
|
|
68
73
|
provide: CoreAuthUserService,
|
|
69
74
|
useClass: UserService,
|
|
@@ -15,6 +15,7 @@ import { CoreAuthModel } from './core-auth.model';
|
|
|
15
15
|
import { LegacyAuthDisabledException } from './exceptions/legacy-auth-disabled.exception';
|
|
16
16
|
import { AuthGuard } from './guards/auth.guard';
|
|
17
17
|
import { LegacyAuthRateLimitGuard } from './guards/legacy-auth-rate-limit.guard';
|
|
18
|
+
import { isLegacyEndpointEnabled } from './helpers/legacy-endpoints.helper';
|
|
18
19
|
import { CoreAuthSignInInput } from './inputs/core-auth-sign-in.input';
|
|
19
20
|
import { CoreAuthSignUpInput } from './inputs/core-auth-sign-up.input';
|
|
20
21
|
import { ICoreAuthUser } from './interfaces/core-auth-user.interface';
|
|
@@ -62,23 +63,15 @@ export class CoreAuthResolver {
|
|
|
62
63
|
/**
|
|
63
64
|
* Check if legacy GraphQL endpoints are enabled
|
|
64
65
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* - config.auth.legacyEndpoints.graphql is false
|
|
66
|
+
* Since 11.38.0 legacy endpoints are OFF unless a project asks for them — see
|
|
67
|
+
* `isLegacyEndpointEnabled()` for the full resolution table.
|
|
68
68
|
*
|
|
69
69
|
* @throws LegacyAuthDisabledException
|
|
70
70
|
*/
|
|
71
71
|
protected checkLegacyGraphQLEnabled(endpointName: string): void {
|
|
72
72
|
const authConfig = this.configService.getFastButReadOnly('auth');
|
|
73
|
-
const legacyConfig = authConfig?.legacyEndpoints;
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
if (legacyConfig?.enabled === false) {
|
|
77
|
-
throw new LegacyAuthDisabledException(endpointName);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check if GraphQL endpoints specifically are disabled
|
|
81
|
-
if (legacyConfig?.graphql === false) {
|
|
74
|
+
if (!isLegacyEndpointEnabled(authConfig?.legacyEndpoints, 'graphql')) {
|
|
82
75
|
throw new LegacyAuthDisabledException(endpointName);
|
|
83
76
|
}
|
|
84
77
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Injectable, Logger, OnApplicationBootstrap, Optional } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
4
|
+
import { CoreBetterAuthUserMapper } from '../better-auth/core-better-auth-user.mapper';
|
|
5
|
+
import { isLegacyEndpointEnabled } from './helpers/legacy-endpoints.helper';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Says out loud, once at boot, that this deployment still exposes Legacy Auth.
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS EXISTS
|
|
11
|
+
*
|
|
12
|
+
* Legacy Auth is superseded by IAM (Better-Auth) and slated for removal (see
|
|
13
|
+
* `.claude/rules/module-deprecation.md`). Until 11.38.0 nothing ever mentioned that a
|
|
14
|
+
* project was still running it: `auth.legacyEndpoints.enabled` defaulted to `true`, so a
|
|
15
|
+
* second fully functional password-authentication surface stayed open by inertia, and
|
|
16
|
+
* `betterAuthMigrationStatus.canDisableLegacyAuth` — which exists precisely to say when
|
|
17
|
+
* it can be closed — only answered when somebody thought to ask.
|
|
18
|
+
*
|
|
19
|
+
* Registered only by `CoreAuthModule`, which exists only when a project passes the
|
|
20
|
+
* legacy three-argument `CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(...),
|
|
21
|
+
* envConfig)`. An IAM-only project never sees any of this.
|
|
22
|
+
*
|
|
23
|
+
* It only REPORTS. Turning legacy auth off for a project that still needs it would lock
|
|
24
|
+
* out every user who has not signed in through IAM yet, which is the opposite of a safe
|
|
25
|
+
* default — the migration has to be finished before the door closes, and only the
|
|
26
|
+
* operator can know when that is.
|
|
27
|
+
*/
|
|
28
|
+
@Injectable()
|
|
29
|
+
export class CoreLegacyAuthDeprecationInitializer implements OnApplicationBootstrap {
|
|
30
|
+
protected readonly logger = new Logger(CoreLegacyAuthDeprecationInitializer.name);
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
protected readonly configService: ConfigService,
|
|
34
|
+
@Optional() protected readonly userMapper?: CoreBetterAuthUserMapper,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
onApplicationBootstrap(): void {
|
|
38
|
+
const legacyConfig = this.configService.getFastButReadOnly('auth')?.legacyEndpoints;
|
|
39
|
+
const graphqlOn = isLegacyEndpointEnabled(legacyConfig, 'graphql');
|
|
40
|
+
const restOn = isLegacyEndpointEnabled(legacyConfig, 'rest');
|
|
41
|
+
|
|
42
|
+
if (!graphqlOn && !restOn) {
|
|
43
|
+
// Every endpoint is closed. That is the state the migration aims at — but it is only
|
|
44
|
+
// GOOD if the migration is actually finished, and 11.38.0 made this the DEFAULT. So a
|
|
45
|
+
// project that upgraded without deciding lands here whether or not its users can still
|
|
46
|
+
// sign in.
|
|
47
|
+
//
|
|
48
|
+
// Reporting only the enabled case would mean the warning fires in the safe state and
|
|
49
|
+
// stays silent in the dangerous one. This branch says the other half: you just closed a
|
|
50
|
+
// door some of your users still need.
|
|
51
|
+
void this.reportLockoutRisk();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const surfaces = [graphqlOn && 'GraphQL', restOn && 'REST'].filter(Boolean).join(' + ');
|
|
56
|
+
this.logger.warn(
|
|
57
|
+
`Legacy Auth is ENABLED (${surfaces}). It is deprecated in favour of IAM (Better-Auth) and will be removed. ` +
|
|
58
|
+
'Close it with auth.legacyEndpoints.enabled: false once your users are migrated.',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Say how far the migration actually is, so the warning carries a next step rather
|
|
62
|
+
// than just a verdict.
|
|
63
|
+
//
|
|
64
|
+
// Deliberately NOT awaited. Nest waits for every onApplicationBootstrap hook before
|
|
65
|
+
// `listen()` resolves, and `getMigrationStatus()` is collection-scale work — so awaiting it
|
|
66
|
+
// would add to time-to-ready on every pod of every rolling deploy, for a log line. A
|
|
67
|
+
// readiness probe that fails while the server counts users is a worse outcome than a
|
|
68
|
+
// warning that arrives a second late.
|
|
69
|
+
void this.reportMigrationStatus();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Warns when legacy auth is closed while users still depend on it.
|
|
74
|
+
*
|
|
75
|
+
* The counterpart to the deprecation warning: that one fires when the door is open, this one
|
|
76
|
+
* when it was just shut on people who cannot get in any other way. Best-effort and detached
|
|
77
|
+
* from the boot, for the same reason as {@link reportMigrationStatus}.
|
|
78
|
+
*/
|
|
79
|
+
protected async reportLockoutRisk(): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
const status = await this.userMapper?.getMigrationStatus?.({ includePendingEmails: false });
|
|
82
|
+
if (!status || status.totalUsers === 0 || status.canDisableLegacyAuth) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.logger.warn(
|
|
87
|
+
`Legacy Auth is DISABLED, but only ${status.fullyMigratedUsers}/${status.totalUsers} users ` +
|
|
88
|
+
`(${status.migrationPercentage}%) are migrated to IAM — the rest cannot sign in at all. ` +
|
|
89
|
+
'Set auth.legacyEndpoints.enabled: true (LEGACY_AUTH_ENABLED=true) until this reaches 100%.',
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.logger.debug(
|
|
93
|
+
`Could not read the IAM migration status: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reads and logs the IAM migration progress. Best-effort by construction: it runs detached
|
|
100
|
+
* from the boot, and a failure is reported at debug level rather than surfaced.
|
|
101
|
+
*/
|
|
102
|
+
protected async reportMigrationStatus(): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
// `includePendingEmails: false` — this reporter needs the counts, never the addresses.
|
|
105
|
+
// Collecting them costs two more collection-scale queries, one a guaranteed COLLSCAN,
|
|
106
|
+
// and handling PII that is then discarded.
|
|
107
|
+
const status = await this.userMapper?.getMigrationStatus?.({ includePendingEmails: false });
|
|
108
|
+
if (!status) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (status.canDisableLegacyAuth) {
|
|
112
|
+
this.logger.warn(
|
|
113
|
+
`All ${status.totalUsers} users are migrated to IAM — Legacy Auth can be switched off now ` +
|
|
114
|
+
'(auth.legacyEndpoints.enabled: false).',
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
this.logger.log(
|
|
118
|
+
`IAM migration status: ${status.fullyMigratedUsers}/${status.totalUsers} users migrated ` +
|
|
119
|
+
`(${status.migrationPercentage}%). Legacy Auth stays required until this reaches 100%.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.logger.debug(
|
|
124
|
+
`Could not read the IAM migration status: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { IAuthLegacyEndpoints } from '../../../common/interfaces/server-options.interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The two transports legacy auth is reachable over.
|
|
5
|
+
*/
|
|
6
|
+
export type LegacyEndpointTransport = 'graphql' | 'rest';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decides whether the legacy auth endpoints are reachable over one transport.
|
|
10
|
+
*
|
|
11
|
+
* ONE resolver for both transports on purpose: the GraphQL resolver and the REST
|
|
12
|
+
* controller each carried their own copy of this decision, and a rule about which auth
|
|
13
|
+
* endpoints exist is exactly the kind of rule that must not be able to answer
|
|
14
|
+
* differently depending on which door somebody knocks on.
|
|
15
|
+
*
|
|
16
|
+
* **Since 11.38.0 the default is OFF.** Legacy auth is superseded by IAM
|
|
17
|
+
* (Better-Auth), which every project created from the starter has used for a while:
|
|
18
|
+
* `CoreModule.forRoot(envConfig)` never registers the legacy module at all. What
|
|
19
|
+
* changed is the answer for a project that DOES register it and never made a decision
|
|
20
|
+
* about `legacyEndpoints` — previously it kept a second, fully functional
|
|
21
|
+
* password-authentication surface open indefinitely, silently. A second way in that
|
|
22
|
+
* nobody chose is a liability, so it now has to be asked for.
|
|
23
|
+
*
|
|
24
|
+
* Resolution order:
|
|
25
|
+
*
|
|
26
|
+
* | Configuration | Result |
|
|
27
|
+
* |---------------|--------|
|
|
28
|
+
* | `enabled: false` | off, whatever the per-transport flags say |
|
|
29
|
+
* | per-transport flag set (`graphql` / `rest`) | that flag wins |
|
|
30
|
+
* | `enabled: true` | on |
|
|
31
|
+
* | nothing set | **off** (was: on, before 11.38.0) |
|
|
32
|
+
*
|
|
33
|
+
* `enabled: false` stays a hard off switch rather than something a per-transport `true`
|
|
34
|
+
* can reopen: it is the setting a project reached for to close legacy down, and an
|
|
35
|
+
* upgrade must never widen it.
|
|
36
|
+
*
|
|
37
|
+
* @param legacyConfig the `auth.legacyEndpoints` config block, if any
|
|
38
|
+
* @param transport which transport is asking
|
|
39
|
+
* @returns `true` when the legacy endpoints are reachable over that transport, `false` otherwise
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // In a project that overrides the check on its own resolver
|
|
44
|
+
* protected override checkLegacyGraphQLEnabled(endpointName: string): void {
|
|
45
|
+
* const legacyConfig = this.configService.getFastButReadOnly('auth')?.legacyEndpoints;
|
|
46
|
+
* if (!isLegacyEndpointEnabled(legacyConfig, 'graphql')) {
|
|
47
|
+
* throw new LegacyAuthDisabledException(endpointName);
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Call this rather than re-implementing the table: the two transports resolving the same rule
|
|
53
|
+
* differently is the defect the shared function exists to prevent.
|
|
54
|
+
*/
|
|
55
|
+
export function isLegacyEndpointEnabled(
|
|
56
|
+
legacyConfig: IAuthLegacyEndpoints | undefined,
|
|
57
|
+
transport: LegacyEndpointTransport,
|
|
58
|
+
): boolean {
|
|
59
|
+
if (legacyConfig?.enabled === false) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const perTransport = legacyConfig?.[transport];
|
|
64
|
+
if (typeof perTransport === 'boolean') {
|
|
65
|
+
return perTransport;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return legacyConfig?.enabled === true;
|
|
69
|
+
}
|
|
@@ -135,8 +135,13 @@ The `CoreBetterAuthUserMapper` enables bidirectional password synchronization:
|
|
|
135
135
|
|
|
136
136
|
- User signs up via BetterAuth → password synced to Legacy Auth (bcrypt hash)
|
|
137
137
|
- User changes password → synced between both systems
|
|
138
|
+
- User resets via IAM → mirrored into the legacy store (11.38.0+; see Password Reset below)
|
|
139
|
+
- User resets via the legacy user endpoint → synced into the IAM credential
|
|
138
140
|
- **Without this, users can only authenticate via ONE system!**
|
|
139
141
|
|
|
142
|
+
The mapper is also what the IAM→legacy reset mirror runs through. Without it registered, that
|
|
143
|
+
mirror is skipped — the reset itself still succeeds, and a warning names the divergence.
|
|
144
|
+
|
|
140
145
|
---
|
|
141
146
|
|
|
142
147
|
### 5. Update ServerModule
|
|
@@ -315,6 +320,23 @@ Check these four things when integrating:
|
|
|
315
320
|
60 s), which is the axis that matters against mail-bombing one victim from rotating IPs.
|
|
316
321
|
- [ ] **Your reset page exists and points at `POST /iam/reset-password`** with `{ token, newPassword }`.
|
|
317
322
|
Tokens are valid for 1 h by default.
|
|
323
|
+
- [ ] **The IAM→legacy mirror is wired (11.38.0+).** `emailAndPassword.onPasswordReset` mirrors an
|
|
324
|
+
applied IAM reset into the legacy bcrypt store, for every native reset route (token,
|
|
325
|
+
email-OTP, phone-number), so a deployment running both systems does not keep the OLD
|
|
326
|
+
password valid on the legacy path — including after a reset performed _because_ it leaked.
|
|
327
|
+
Automatic, but it needs `CoreBetterAuthUserMapper` resolvable (see above), and it is skipped
|
|
328
|
+
entirely when no legacy endpoint is enabled. A skipped mirror logs
|
|
329
|
+
`Could not mirror the IAM password reset to the legacy store` — if you see that, the mapper
|
|
330
|
+
is missing, not the reset.
|
|
331
|
+
- [ ] **You did not add your own `options.emailAndPassword.onPasswordReset`** expecting it to
|
|
332
|
+
replace the framework's. It cannot: the two are CHAINED, framework first. That is
|
|
333
|
+
deliberate — a project hook is an addition, never a way to silently switch off credential-
|
|
334
|
+
store parity.
|
|
335
|
+
- [ ] **Password length is enforced client-side too.** The middleware normalizes a plaintext reset
|
|
336
|
+
password to sha256 before Better-Auth sees it, so Better-Auth's own
|
|
337
|
+
`minPasswordLength`/`maxPasswordLength` cannot fire. nest-server checks the raw value it
|
|
338
|
+
receives — but a client that hashes before sending (every lt frontend does) presents a
|
|
339
|
+
64-hex string that carries no length information. The policy has to live in the form.
|
|
318
340
|
- [ ] **`trustedOrigins` contains no wildcard.** `redirectTo` is validated against it and the reset
|
|
319
341
|
redirect carries the token, so a wildcard hands a live token to any origin it admits. The
|
|
320
342
|
framework warns at boot if it finds one.
|
|
@@ -1745,23 +1745,37 @@ When both Legacy Auth and BetterAuth (IAM) are active, passwords are automatical
|
|
|
1745
1745
|
| Sign up via BetterAuth | IAM | Legacy | ✅ Yes |
|
|
1746
1746
|
| Sign up via Legacy Auth | Legacy | IAM | ⚠️ On first IAM sign-in |
|
|
1747
1747
|
| Password reset via Legacy | Legacy | IAM | ✅ Yes |
|
|
1748
|
-
| Password reset via BetterAuth | IAM | Legacy |
|
|
1748
|
+
| Password reset via BetterAuth | IAM | Legacy | ✅ Yes (since 11.38.0) |
|
|
1749
1749
|
| Password change via user update | Legacy | IAM | ✅ Yes |
|
|
1750
1750
|
|
|
1751
1751
|
#### IAM Password Reset → Legacy Sync
|
|
1752
1752
|
|
|
1753
|
-
|
|
1753
|
+
**Automatic since 11.38.0. No configuration, and nothing to implement.**
|
|
1754
1754
|
|
|
1755
|
-
|
|
1755
|
+
This used to be the one direction the framework could not cover, on the reasoning that the
|
|
1756
|
+
plain password is gone once BetterAuth has hashed it with scrypt. That is true of the hash,
|
|
1757
|
+
but the password is still in the request — and the two halves needed to mirror it are simply
|
|
1758
|
+
known in two different places:
|
|
1756
1759
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1760
|
+
| half | known by |
|
|
1761
|
+
| ------------------------ | ------------------------------------------------------------------------ |
|
|
1762
|
+
| the new password | `CoreBetterAuthApiMiddleware`, which sees the request body |
|
|
1763
|
+
| which user it belongs to | `emailAndPassword.onPasswordReset`, which BetterAuth calls with the user |
|
|
1759
1764
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1765
|
+
The middleware normalizes the password on the native reset routes and puts it on an
|
|
1766
|
+
`AsyncLocalStorage` context scoped to exactly that handler call
|
|
1767
|
+
(`core-better-auth-password-reset.registry.ts`); the `onPasswordReset` hook reads it there and
|
|
1768
|
+
calls `syncPasswordToLegacy()`. The mirror therefore covers **every** native reset route —
|
|
1769
|
+
token, email-OTP and phone-number — and writes the same value BetterAuth just hashed, so the
|
|
1770
|
+
two stores cannot drift.
|
|
1762
1771
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1772
|
+
Failure is non-fatal by design: the hook is wrapped, and a failed mirror is logged rather than
|
|
1773
|
+
turned into a failed reset. The IAM credential is already written at that point, and telling a
|
|
1774
|
+
user their reset did not work when it half did is the worse outcome.
|
|
1775
|
+
|
|
1776
|
+
> **Why this matters beyond convenience.** Before 11.38.0 a reset performed _because a password
|
|
1777
|
+
> leaked_ left that password valid on the legacy path. Two stores that disagree about a
|
|
1778
|
+
> credential is not an inconvenience — it is the old password still working somewhere.
|
|
1765
1779
|
|
|
1766
1780
|
### Automatic Sync (No Configuration Required)
|
|
1767
1781
|
|
|
@@ -1982,57 +1996,16 @@ await betterAuthUserMapper.migrateAccountToIam(email, plainPassword);
|
|
|
1982
1996
|
|
|
1983
1997
|
### Custom Password Reset with Sync
|
|
1984
1998
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
import { Body, Controller, Post } from '@nestjs/common';
|
|
1990
|
-
import { CoreBetterAuthController, CoreBetterAuthUserMapper, Roles, RoleEnum } from '@lenne.tech/nest-server';
|
|
1991
|
-
|
|
1992
|
-
@Controller('iam')
|
|
1993
|
-
export class BetterAuthController extends CoreBetterAuthController {
|
|
1994
|
-
constructor(
|
|
1995
|
-
// ... other dependencies
|
|
1996
|
-
private readonly betterAuthUserMapper: CoreBetterAuthUserMapper,
|
|
1997
|
-
) {
|
|
1998
|
-
super(...);
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
/**
|
|
2002
|
-
* Custom password reset that syncs to both auth systems
|
|
2003
|
-
*/
|
|
2004
|
-
@Post('reset-password-sync')
|
|
2005
|
-
@Roles(RoleEnum.S_EVERYONE)
|
|
2006
|
-
async resetPasswordWithSync(
|
|
2007
|
-
@Body() input: { token: string; newPassword: string },
|
|
2008
|
-
): Promise<{ success: boolean }> {
|
|
2009
|
-
// 1. Reset password via BetterAuth native API
|
|
2010
|
-
const api = this.betterAuthService.getApi();
|
|
2011
|
-
await api.resetPassword({
|
|
2012
|
-
body: { token: input.token, newPassword: input.newPassword },
|
|
2013
|
-
});
|
|
2014
|
-
|
|
2015
|
-
// 2. Sync to Legacy Auth using the plain password
|
|
2016
|
-
// Get user email from the token (you may need to decode it or lookup)
|
|
2017
|
-
const userEmail = await this.getUserEmailFromToken(input.token);
|
|
2018
|
-
if (userEmail) {
|
|
2019
|
-
await this.betterAuthUserMapper.syncPasswordToLegacy(
|
|
2020
|
-
'', // iamUserId not needed for email lookup
|
|
2021
|
-
userEmail,
|
|
2022
|
-
input.newPassword,
|
|
2023
|
-
);
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
return { success: true };
|
|
2027
|
-
}
|
|
1999
|
+
**Retired in 11.38.0 — do not build this.** This section used to carry a custom
|
|
2000
|
+
`reset-password-sync` controller endpoint, because the framework could not mirror an IAM reset
|
|
2001
|
+
into the legacy store. It now does, automatically, for every native reset route (see
|
|
2002
|
+
[IAM Password Reset → Legacy Sync](#iam-password-reset--legacy-sync)).
|
|
2028
2003
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
}
|
|
2035
|
-
```
|
|
2004
|
+
Re-implementing it is now actively harmful: the middleware has already normalized the body, so
|
|
2005
|
+
a hand-rolled endpoint that calls `normalizePasswordForIam()` again would hash an
|
|
2006
|
+
already-hashed value and store a credential nothing can verify. If you carry such an override
|
|
2007
|
+
from an earlier version, remove it — the framework mirror runs either way, and the second write
|
|
2008
|
+
is what breaks.
|
|
2036
2009
|
|
|
2037
2010
|
**Alternative: Use Legacy Password Reset**
|
|
2038
2011
|
|