@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.
- package/.claude/rules/configurable-features.md +3 -0
- package/CLAUDE.md +1 -0
- package/FRAMEWORK-API.md +3 -2
- package/dist/core/common/helpers/logging.helper.js +1 -1
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +3 -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.d.ts +5 -2
- package/dist/core/modules/better-auth/better-auth.config.js +35 -2
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +15 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +115 -9
- 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-rate-limiter.service.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +14 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.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/core/modules/better-auth/core-better-auth.module.js +19 -5
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +3 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.service.js +11 -2
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/templates/password-reset-de.ejs +65 -0
- package/dist/templates/password-reset-en.ejs +65 -0
- package/dist/templates/password-reset.ejs +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/security-overrides.md +17 -11
- package/migration-guides/11.35.1-to-11.36.0.md +168 -0
- package/migration-guides/11.36.0-to-11.36.1.md +283 -0
- package/package.json +25 -25
- package/src/core/common/helpers/logging.helper.ts +9 -1
- package/src/core/common/interfaces/server-options.interface.ts +68 -2
- package/src/core/common/services/brevo.service.ts +34 -6
- package/src/core/modules/better-auth/CUSTOMIZATION.md +33 -7
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +38 -0
- package/src/core/modules/better-auth/README.md +100 -15
- package/src/core/modules/better-auth/better-auth.config.ts +142 -9
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +265 -20
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +30 -2
- package/src/core/modules/better-auth/core-better-auth.controller.ts +38 -0
- package/src/core/modules/better-auth/core-better-auth.module.ts +45 -13
- package/src/core/modules/better-auth/core-better-auth.service.ts +10 -2
- package/src/core/modules/tenant/core-tenant.service.ts +41 -4
- package/src/templates/password-reset-de.ejs +65 -0
- package/src/templates/password-reset-en.ejs +65 -0
- 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
|
|
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
|
-
|
|
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.
|
|
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>
|