@lenne.tech/nest-server 11.35.1 → 11.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude/rules/configurable-features.md +3 -0
  2. package/CLAUDE.md +1 -0
  3. package/FRAMEWORK-API.md +3 -2
  4. package/dist/core/common/helpers/logging.helper.js +1 -1
  5. package/dist/core/common/helpers/logging.helper.js.map +1 -1
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
  7. package/dist/core/common/services/brevo.service.d.ts +1 -0
  8. package/dist/core/common/services/brevo.service.js +10 -2
  9. package/dist/core/common/services/brevo.service.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth.config.d.ts +5 -2
  11. package/dist/core/modules/better-auth/better-auth.config.js +35 -2
  12. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  13. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +15 -1
  14. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +115 -9
  15. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  16. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  17. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +14 -1
  18. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth.controller.js +19 -0
  20. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth.module.js +19 -5
  22. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  23. package/dist/core/modules/better-auth/core-better-auth.service.js +3 -1
  24. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  25. package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
  26. package/dist/core/modules/tenant/core-tenant.service.js +11 -2
  27. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  28. package/dist/templates/password-reset-de.ejs +65 -0
  29. package/dist/templates/password-reset-en.ejs +65 -0
  30. package/dist/templates/password-reset.ejs +1 -1
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/docs/security-overrides.md +17 -11
  33. package/migration-guides/11.35.1-to-11.36.0.md +168 -0
  34. package/migration-guides/11.36.0-to-11.36.1.md +283 -0
  35. package/package.json +25 -25
  36. package/src/core/common/helpers/logging.helper.ts +9 -1
  37. package/src/core/common/interfaces/server-options.interface.ts +68 -2
  38. package/src/core/common/services/brevo.service.ts +34 -6
  39. package/src/core/modules/better-auth/CUSTOMIZATION.md +33 -7
  40. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +38 -0
  41. package/src/core/modules/better-auth/README.md +100 -15
  42. package/src/core/modules/better-auth/better-auth.config.ts +142 -9
  43. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +265 -20
  44. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +30 -2
  45. package/src/core/modules/better-auth/core-better-auth.controller.ts +38 -0
  46. package/src/core/modules/better-auth/core-better-auth.module.ts +45 -13
  47. package/src/core/modules/better-auth/core-better-auth.service.ts +10 -2
  48. package/src/core/modules/tenant/core-tenant.service.ts +41 -4
  49. package/src/templates/password-reset-de.ejs +65 -0
  50. package/src/templates/password-reset-en.ejs +65 -0
  51. package/src/templates/password-reset.ejs +1 -1
@@ -74,7 +74,22 @@ export class CoreTenantService {
74
74
  }
75
75
 
76
76
  /**
77
- * Get a single membership (any status).
77
+ * Get a single membership REGARDLESS of its status.
78
+ *
79
+ * Read the name literally: a removal is a status change to `SUSPENDED`, not
80
+ * a delete, so this still answers for somebody who was thrown out. That is
81
+ * deliberate and `addMember` depends on it — it reactivates the existing row
82
+ * instead of creating a duplicate.
83
+ *
84
+ * It is therefore the WRONG method for an authorization check. Asking "is
85
+ * this user a member with role X?" through it answers yes for a removed
86
+ * member, and a route that guards itself this way keeps granting a
87
+ * suspended administrator the right to invite, remove and re-role — the very
88
+ * rights that being removed was supposed to take away. Routes that carry
89
+ * `@SkipTenantCheck()` and decide for themselves are exactly the ones at
90
+ * risk, because the tenant guard never sees them.
91
+ *
92
+ * For an authorization check use {@link getActiveMembership}.
78
93
  */
79
94
  async getMembership(tenantId: string, userId: string): Promise<CoreTenantMemberModel | null> {
80
95
  return this.memberModel
@@ -83,6 +98,25 @@ export class CoreTenantService {
83
98
  .exec() as Promise<CoreTenantMemberModel | null>;
84
99
  }
85
100
 
101
+ /**
102
+ * Get a membership only while it is live — the one to use when deciding what
103
+ * somebody may do.
104
+ *
105
+ * Returns `null` for a suspended or invited membership, so "removed" means
106
+ * removed everywhere, not just for the data the tenant guard happens to
107
+ * cover.
108
+ */
109
+ async getActiveMembership(tenantId: string, userId: string): Promise<CoreTenantMemberModel | null> {
110
+ if (!tenantId?.trim() || !userId?.trim()) {
111
+ return null;
112
+ }
113
+
114
+ return this.memberModel
115
+ .findOne({ status: TenantMemberStatus.ACTIVE, tenant: tenantId, user: userId })
116
+ .lean()
117
+ .exec() as Promise<CoreTenantMemberModel | null>;
118
+ }
119
+
86
120
  /**
87
121
  * Add a member to a tenant.
88
122
  * Uses bypassTenantGuard to avoid tenant filtering on the membership collection itself.
@@ -193,8 +227,11 @@ export class CoreTenantService {
193
227
  assertAssignableMembershipRole(role);
194
228
  const highestRole = this.getHighestRole();
195
229
 
196
- // If demoting from highest role, ensure it's not the last one
197
- const existing = await this.getMembership(tenantId, userId);
230
+ // If demoting from highest role, ensure it's not the last one. Active
231
+ // only: a suspended membership is not an owner any more, and letting it
232
+ // trigger the guard produces "cannot demote the last owner" for a user who
233
+ // is not even a member.
234
+ const existing = await this.getActiveMembership(tenantId, userId);
198
235
  if (existing?.role === highestRole && role !== highestRole) {
199
236
  await this.assertNotLastOwner(tenantId, userId);
200
237
  }
@@ -237,7 +274,7 @@ export class CoreTenantService {
237
274
  });
238
275
 
239
276
  if (ownerCount <= 1) {
240
- const membership = await this.getMembership(tenantId, userId);
277
+ const membership = await this.getActiveMembership(tenantId, userId);
241
278
  if (membership?.role === highestRole) {
242
279
  throw new BadRequestException('Cannot remove or demote the last owner of a tenant');
243
280
  }
@@ -0,0 +1,65 @@
1
+ <% /* `appName` is optional so a caller that omits it cannot turn a password-reset mail into a 500. */ const brand = (typeof appName !== 'undefined' && appName) ? appName : ''; -%>
2
+ <!DOCTYPE html>
3
+ <html lang="de">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
+ <title>Passwort zurücksetzen</title>
9
+ <style>
10
+ /* Responsive: on phones let the card fill the width and tighten the side
11
+ padding. Clients that ignore <style>/@media still get a fluid card via
12
+ the inline width:100% + max-width:600px below. */
13
+ @media only screen and (max-width: 600px) {
14
+ .ns-px { padding-left: 24px !important; padding-right: 24px !important; }
15
+ .ns-logo { width: 160px !important; height: auto !important; }
16
+ }
17
+ </style>
18
+ </head>
19
+ <body style="margin:0; padding:0; background-color:#f8fafc; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;">
20
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f8fafc" style="background-color:#f8fafc;">
21
+ <tr>
22
+ <td align="center" style="padding:32px 16px;">
23
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" style="width:100%; max-width:600px; background-color:#ffffff; border:1px solid #e2e8f0; border-radius:8px; overflow:hidden;">
24
+ <!-- Header: the logo when the deployment supplies one, the app name otherwise -->
25
+ <tr>
26
+ <td align="center" bgcolor="#ffffff" style="background-color:#ffffff; padding:32px 32px 24px 32px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
27
+ <% if (typeof logoSrc !== 'undefined' && logoSrc) { %>
28
+ <img src="<%= logoSrc %>" alt="<%= brand %>" class="ns-logo" width="200" style="display:block; width:200px; height:auto; border:0; text-decoration:none;">
29
+ <% } else { %>
30
+ <span style="font-size:20px; line-height:26px; font-weight:700; color:#262626;"><%= brand %></span>
31
+ <% } %>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td class="ns-px" style="padding:36px 40px 8px 40px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
36
+ <h1 style="margin:0 0 16px 0; font-size:22px; line-height:28px; font-weight:700; color:#262626;">Hallo <%= name %>,</h1>
37
+ <p style="margin:0 0 16px 0; font-size:16px; line-height:24px; color:#262626;">du hast ein neues Passwort angefordert. Über die Schaltfläche unten kannst du eines festlegen.</p>
38
+ </td>
39
+ </tr>
40
+ <tr>
41
+ <td class="ns-px" style="padding:8px 40px 36px 40px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
42
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0">
43
+ <tr>
44
+ <td align="center" bgcolor="#2563eb" style="border-radius:6px; mso-padding-alt:13px 28px;">
45
+ <a href="<%= link %>" target="_blank" style="display:inline-block; padding:13px 28px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; font-size:16px; line-height:20px; font-weight:600; color:#ffffff; text-decoration:none; border-radius:6px;">Passwort zurücksetzen</a>
46
+ </td>
47
+ </tr>
48
+ </table>
49
+ <p style="margin:24px 0 0 0; font-size:13px; line-height:20px; color:#737373;">
50
+ Falls die Schaltfläche nicht funktioniert, kopiere diesen Link in deinen Browser:<br>
51
+ <a href="<%= link %>" style="color:#1d4ed8; word-break:break-all; overflow-wrap:break-word;"><%= link %></a>
52
+ </p>
53
+ </td>
54
+ </tr>
55
+ <tr>
56
+ <td class="ns-px" style="padding:24px 40px 32px 40px; border-top:1px solid #e2e8f0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
57
+ <p style="margin:0; font-size:12px; line-height:18px; color:#a3a3a3;">Dies ist eine automatisch versendete Nachricht <% if (brand) { %>zu deinem <%= brand %>-Konto<% } else { %>zu deinem Konto<% } %>. Wenn du kein neues Passwort angefordert hast, kannst du diese E-Mail ignorieren — dein Passwort bleibt unverändert.</p>
58
+ </td>
59
+ </tr>
60
+ </table>
61
+ </td>
62
+ </tr>
63
+ </table>
64
+ </body>
65
+ </html>
@@ -0,0 +1,65 @@
1
+ <% /* `appName` is optional so a caller that omits it cannot turn a password-reset mail into a 500. */ const brand = (typeof appName !== 'undefined' && appName) ? appName : ''; -%>
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
+ <title>Reset your password</title>
9
+ <style>
10
+ /* Responsive: on phones let the card fill the width and tighten the side
11
+ padding. Clients that ignore <style>/@media still get a fluid card via
12
+ the inline width:100% + max-width:600px below. */
13
+ @media only screen and (max-width: 600px) {
14
+ .ns-px { padding-left: 24px !important; padding-right: 24px !important; }
15
+ .ns-logo { width: 160px !important; height: auto !important; }
16
+ }
17
+ </style>
18
+ </head>
19
+ <body style="margin:0; padding:0; background-color:#f8fafc; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;">
20
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f8fafc" style="background-color:#f8fafc;">
21
+ <tr>
22
+ <td align="center" style="padding:32px 16px;">
23
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" style="width:100%; max-width:600px; background-color:#ffffff; border:1px solid #e2e8f0; border-radius:8px; overflow:hidden;">
24
+ <!-- Header: the logo when the deployment supplies one, the app name otherwise -->
25
+ <tr>
26
+ <td align="center" bgcolor="#ffffff" style="background-color:#ffffff; padding:32px 32px 24px 32px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
27
+ <% if (typeof logoSrc !== 'undefined' && logoSrc) { %>
28
+ <img src="<%= logoSrc %>" alt="<%= brand %>" class="ns-logo" width="200" style="display:block; width:200px; height:auto; border:0; text-decoration:none;">
29
+ <% } else { %>
30
+ <span style="font-size:20px; line-height:26px; font-weight:700; color:#262626;"><%= brand %></span>
31
+ <% } %>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td class="ns-px" style="padding:36px 40px 8px 40px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
36
+ <h1 style="margin:0 0 16px 0; font-size:22px; line-height:28px; font-weight:700; color:#262626;">Hello <%= name %>,</h1>
37
+ <p style="margin:0 0 16px 0; font-size:16px; line-height:24px; color:#262626;">you asked to set a new password. Use the button below to choose one.</p>
38
+ </td>
39
+ </tr>
40
+ <tr>
41
+ <td class="ns-px" style="padding:8px 40px 36px 40px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
42
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0">
43
+ <tr>
44
+ <td align="center" bgcolor="#2563eb" style="border-radius:6px; mso-padding-alt:13px 28px;">
45
+ <a href="<%= link %>" target="_blank" style="display:inline-block; padding:13px 28px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; font-size:16px; line-height:20px; font-weight:600; color:#ffffff; text-decoration:none; border-radius:6px;">Reset password</a>
46
+ </td>
47
+ </tr>
48
+ </table>
49
+ <p style="margin:24px 0 0 0; font-size:13px; line-height:20px; color:#737373;">
50
+ If the button doesn't work, copy and paste this link into your browser:<br>
51
+ <a href="<%= link %>" style="color:#1d4ed8; word-break:break-all; overflow-wrap:break-word;"><%= link %></a>
52
+ </p>
53
+ </td>
54
+ </tr>
55
+ <tr>
56
+ <td class="ns-px" style="padding:24px 40px 32px 40px; border-top:1px solid #e2e8f0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
57
+ <p style="margin:0; font-size:12px; line-height:18px; color:#a3a3a3;">This is an automated message regarding <% if (brand) { %>your <%= brand %> account<% } else { %>your account<% } %>. If you did not ask for a password reset, you can safely ignore this email — your password stays unchanged.</p>
58
+ </td>
59
+ </tr>
60
+ </table>
61
+ </td>
62
+ </tr>
63
+ </table>
64
+ </body>
65
+ </html>
@@ -1,3 +1,3 @@
1
1
  <h1>Hello <%= name %>,</h1>
2
2
  <p>you requested a link for setting a new password. Here it is.</p><br />
3
- <a href="<%= link %>">Passwort zurücksetzten</a>
3
+ <a href="<%= link %>">Reset password</a>