@stamhoofd/backend 2.137.4 → 2.138.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/package.json +26 -17
- package/src/boot.ts +5 -0
- package/src/crons/balance-emails.ts +2 -1
- package/src/crons/delete-expired-mfa-tokens.ts +35 -0
- package/src/crons/drip-emails.ts +2 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/invoices.ts +5 -3
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +3 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -2
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +135 -7
- package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
- package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
- package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +4 -2
- package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
- package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
- package/src/endpoints/auth/MFA.security.test.ts +688 -0
- package/src/endpoints/auth/MFA.test.ts +1398 -0
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
- package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
- package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
- package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
- package/src/endpoints/auth/SignupEndpoint.ts +17 -4
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +11 -6
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +21 -10
- package/src/helpers/Context.ts +114 -4
- package/src/helpers/EmailBuilder.test.ts +287 -0
- package/src/helpers/EmailBuilder.ts +811 -0
- package/src/helpers/EmailResumer.ts +2 -1
- package/src/helpers/ForwardHandler.ts +2 -1
- package/src/helpers/MFAEncryption.ts +34 -0
- package/src/helpers/MembershipCharger.ts +3 -2
- package/src/helpers/RecoveryCodeHelper.ts +81 -0
- package/src/helpers/TOTPHelper.ts +79 -0
- package/src/helpers/TenantContext.test.ts +96 -0
- package/src/helpers/TenantContext.ts +67 -0
- package/src/helpers/TwoFactorHelper.ts +356 -0
- package/src/helpers/WebauthnHelper.ts +211 -0
- package/src/helpers/data/aaguids.json +1006 -0
- package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
- package/src/middleware/TenantScopeMiddleware.ts +24 -0
- package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
- package/src/services/AdminSessionService.ts +41 -0
- package/src/services/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- package/src/services/EmailSendService.test.ts +1218 -0
- package/src/services/EmailSendService.ts +705 -0
- package/src/services/EventNotificationService.ts +2 -1
- package/src/services/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -1
- package/src/services/InvoicePdfService.ts +3 -3
- package/src/services/InvoiceService.ts +4 -2
- package/src/services/InvoiceXMLService.ts +2 -1
- package/src/services/OrderService.test.ts +308 -0
- package/src/services/OrderService.ts +215 -0
- package/src/services/OrganizationAdminService.test.ts +47 -0
- package/src/services/OrganizationAdminService.ts +139 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +212 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PaymentService.ts +5 -3
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/ReferralService.ts +3 -2
- package/src/services/RegistrationService.ts +4 -2
- package/src/services/SSOService.ts +106 -14
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +115 -1
- package/src/services/TwoFactorAuditLogService.ts +64 -0
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +101 -0
- package/tests/e2e/api-rate-limits.test.ts +1 -1
- package/tests/e2e/documents.test.ts +2 -1
- package/tests/e2e/private-files.test.ts +77 -14
- package/tests/helpers/MFATestHelper.ts +42 -0
- package/tests/helpers/TestServer.ts +4 -0
- package/tests/helpers/index.ts +1 -0
- package/tsconfig.build.json +2 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { GetEmailIdentityCommandOutput } from '@aws-sdk/client-sesv2';
|
|
2
|
+
import { CreateEmailIdentityCommand, DeleteEmailIdentityCommand, GetEmailIdentityCommand, PutEmailIdentityFeedbackAttributesCommand, PutEmailIdentityMailFromAttributesCommand, SESv2Client } from '@aws-sdk/client-sesv2';
|
|
3
|
+
import type { Organization } from '@stamhoofd/models';
|
|
4
|
+
import { validateDNSRecords } from '@stamhoofd/models/helpers/DNSValidator.js';
|
|
5
|
+
import { DNSRecordStatus, EmailTemplateType } from '@stamhoofd/structures';
|
|
6
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
import { OrganizationEmailService } from './OrganizationEmailService.js';
|
|
8
|
+
|
|
9
|
+
export class OrganizationDNSService {
|
|
10
|
+
static async updateDNSRecords(organization: Organization) {
|
|
11
|
+
// Check initial status
|
|
12
|
+
let isValidRecords = true;
|
|
13
|
+
for (const record of organization.privateMeta.dnsRecords) {
|
|
14
|
+
if (record.status !== DNSRecordStatus.Valid) {
|
|
15
|
+
isValidRecords = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { allValid } = await validateDNSRecords(organization.privateMeta.dnsRecords);
|
|
20
|
+
|
|
21
|
+
if (organization.registerDomain ?? organization.privateMeta.pendingRegisterDomain) {
|
|
22
|
+
const registerDomainRecord = (organization.privateMeta.pendingRegisterDomain ?? organization.registerDomain) + '.';
|
|
23
|
+
const records = organization.privateMeta.dnsRecords.filter(r => r.name === registerDomainRecord);
|
|
24
|
+
const areRegisterDomainRecordsValid = records.length === 0 || records.every(r => r.status === DNSRecordStatus.Valid);
|
|
25
|
+
|
|
26
|
+
if (areRegisterDomainRecordsValid) {
|
|
27
|
+
// We can setup the register domain if needed
|
|
28
|
+
if (organization.privateMeta.pendingRegisterDomain !== null) {
|
|
29
|
+
organization.registerDomain = organization.privateMeta.pendingRegisterDomain;
|
|
30
|
+
organization.privateMeta.pendingRegisterDomain = null;
|
|
31
|
+
|
|
32
|
+
console.log('Did set register domain for ' + organization.id + ' to ' + organization.registerDomain);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// Clear register domain
|
|
36
|
+
if (organization.registerDomain) {
|
|
37
|
+
// We need to clear it, to prevent sending e-mails with invalid links
|
|
38
|
+
organization.privateMeta.pendingRegisterDomain = organization.privateMeta.pendingRegisterDomain ?? organization.registerDomain;
|
|
39
|
+
organization.registerDomain = null;
|
|
40
|
+
|
|
41
|
+
console.log('Cleared register domain for ' + organization.id + ' because of invalid non txt records');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (allValid) {
|
|
47
|
+
if (organization.privateMeta.pendingMailDomain !== null) {
|
|
48
|
+
organization.privateMeta.mailDomain = organization.privateMeta.pendingMailDomain;
|
|
49
|
+
organization.privateMeta.pendingMailDomain = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const wasUnstable = organization.serverMeta.isDNSUnstable;
|
|
53
|
+
organization.serverMeta.markDNSValid();
|
|
54
|
+
|
|
55
|
+
const didSendDomainSetupMail = organization.serverMeta.didSendDomainSetupMail;
|
|
56
|
+
const didSendWarning = organization.serverMeta.DNSRecordWarningCount > 0;
|
|
57
|
+
organization.serverMeta.DNSRecordWarningCount = 0;
|
|
58
|
+
|
|
59
|
+
const wasActive = organization.privateMeta.mailDomainActive;
|
|
60
|
+
await this.updateAWSMailIdenitity(organization);
|
|
61
|
+
|
|
62
|
+
// yay! Do not Save until after doing AWS changes
|
|
63
|
+
await organization.save();
|
|
64
|
+
|
|
65
|
+
if (wasUnstable && !organization.serverMeta.isDNSUnstable) {
|
|
66
|
+
console.warn('DNS settings became stable for ' + organization.name + ' ' + organization.id);
|
|
67
|
+
|
|
68
|
+
await OrganizationEmailService.sendEmailTemplate(organization, {
|
|
69
|
+
type: EmailTemplateType.OrganizationStableDNS,
|
|
70
|
+
bcc: true,
|
|
71
|
+
});
|
|
72
|
+
} else if (!wasActive && organization.privateMeta.mailDomainActive && (!didSendDomainSetupMail || didSendWarning) && !organization.serverMeta.isDNSUnstable) {
|
|
73
|
+
organization.serverMeta.didSendDomainSetupMail = true;
|
|
74
|
+
await organization.save();
|
|
75
|
+
|
|
76
|
+
if (!didSendDomainSetupMail) {
|
|
77
|
+
await OrganizationEmailService.sendEmailTemplate(organization, {
|
|
78
|
+
type: EmailTemplateType.OrganizationDNSSetupComplete,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
await OrganizationEmailService.sendEmailTemplate(organization, {
|
|
82
|
+
type: EmailTemplateType.OrganizationValidDNS,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// DNS settings gone broken
|
|
88
|
+
if (organization.privateMeta.mailDomain) {
|
|
89
|
+
organization.privateMeta.pendingMailDomain = organization.privateMeta.pendingMailDomain ?? organization.privateMeta.mailDomain;
|
|
90
|
+
organization.privateMeta.mailDomain = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const wasDNSUnstable = organization.serverMeta.isDNSUnstable;
|
|
94
|
+
|
|
95
|
+
organization.serverMeta.markDNSFailure();
|
|
96
|
+
|
|
97
|
+
// disable AWS emails
|
|
98
|
+
organization.privateMeta.mailDomainActive = false;
|
|
99
|
+
|
|
100
|
+
// save
|
|
101
|
+
await organization.save();
|
|
102
|
+
|
|
103
|
+
if (!wasDNSUnstable && organization.serverMeta.isDNSUnstable) {
|
|
104
|
+
// DNS became instable
|
|
105
|
+
console.warn('DNS settings became instable for ' + organization.name + ' ' + organization.id);
|
|
106
|
+
|
|
107
|
+
await OrganizationEmailService.sendEmailTemplate(organization, {
|
|
108
|
+
type: EmailTemplateType.OrganizationUnstableDNS,
|
|
109
|
+
bcc: true,
|
|
110
|
+
});
|
|
111
|
+
} else if (!organization.serverMeta.isDNSUnstable && organization.serverMeta.didSendDomainSetupMail && organization.serverMeta.DNSRecordWarningCount == 0) {
|
|
112
|
+
organization.serverMeta.DNSRecordWarningCount += 1;
|
|
113
|
+
await organization.save();
|
|
114
|
+
|
|
115
|
+
await OrganizationEmailService.sendEmailTemplate(organization, {
|
|
116
|
+
type: EmailTemplateType.OrganizationInvalidDNS,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static get forbiddenEmailDomains() {
|
|
123
|
+
return [
|
|
124
|
+
STAMHOOFD.domains.dashboard,
|
|
125
|
+
...Object.values(STAMHOOFD.domains.defaultBroadcastEmail ?? {}),
|
|
126
|
+
...Object.values(STAMHOOFD.domains.defaultTransactionalEmail ?? {}),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static async deleteAWSMailIdenitity(organization: Organization, mailDomain: string) {
|
|
131
|
+
// Protect specific domain names
|
|
132
|
+
if (this.forbiddenEmailDomains.includes(mailDomain.toLowerCase())) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (STAMHOOFD.environment !== 'production') {
|
|
137
|
+
// Temporary ignore this
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const client = new SESv2Client({});
|
|
142
|
+
|
|
143
|
+
// Check if mail identitiy already exists..
|
|
144
|
+
try {
|
|
145
|
+
const cmd = new GetEmailIdentityCommand({
|
|
146
|
+
EmailIdentity: mailDomain,
|
|
147
|
+
});
|
|
148
|
+
const result = await client.send(cmd);
|
|
149
|
+
|
|
150
|
+
if (result.VerifiedForSendingStatus === true) {
|
|
151
|
+
console.log('Cant delete AWS mail idenitiy @' + organization.id + ' for ' + mailDomain + ': already validated and might be in use by other organizations');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('Deleting AWS mail identity @' + organization.id + ' for ' + mailDomain);
|
|
156
|
+
|
|
157
|
+
const deleteCmd = new DeleteEmailIdentityCommand({
|
|
158
|
+
EmailIdentity: mailDomain,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await client.send(deleteCmd);
|
|
162
|
+
console.log('Deleted AWS mail idenitiy @' + organization.id + ' for ' + organization.privateMeta.mailDomain);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error('Could not delete AWS email identitiy @' + organization.id + ' for ' + organization.privateMeta.mailDomain);
|
|
165
|
+
console.error(e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create or update the AWS mail idenitiy and also update the active state of the mailDomain
|
|
171
|
+
*/
|
|
172
|
+
static async updateAWSMailIdenitity(organization: Organization) {
|
|
173
|
+
if (organization.privateMeta.mailDomain === null) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Protect specific domain names
|
|
178
|
+
if (['stamhoofd.be', 'stamhoofd.nl', 'stamhoofd.shop', 'stamhoofd.app', 'stamhoofd.email'].includes(organization.privateMeta.mailDomain)) {
|
|
179
|
+
console.error('Tried to validate AWS mail identity with protected domains @' + organization.id);
|
|
180
|
+
organization.privateMeta.mailDomainActive = false;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (STAMHOOFD.environment !== 'production') {
|
|
185
|
+
// Temporary ignore this
|
|
186
|
+
organization.privateMeta.mailDomainActive = true;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const client = new SESv2Client({});
|
|
191
|
+
const expectedConfigurationSetName = Formatter.slug(STAMHOOFD.platformName + '-domains');
|
|
192
|
+
|
|
193
|
+
// Check if mail identitiy already exists..
|
|
194
|
+
let exists = false;
|
|
195
|
+
let existing: GetEmailIdentityCommandOutput | undefined = undefined;
|
|
196
|
+
try {
|
|
197
|
+
const cmd = new GetEmailIdentityCommand({
|
|
198
|
+
EmailIdentity: organization.privateMeta.mailDomain,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
existing = await client.send(cmd);
|
|
202
|
+
exists = true;
|
|
203
|
+
|
|
204
|
+
console.log('AWS mail idenitiy exists already: just checking the verification status in AWS @' + organization.id);
|
|
205
|
+
|
|
206
|
+
if (existing.ConfigurationSetName !== expectedConfigurationSetName) {
|
|
207
|
+
// Not allowed to use this identity
|
|
208
|
+
organization.privateMeta.mailDomainActive = false;
|
|
209
|
+
console.error('Organization is not allowed to use email identity ' + organization.privateMeta.mailDomain + ' @' + organization.id + ', got ' + existing.ConfigurationSetName);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
organization.privateMeta.mailDomainActive = existing.VerifiedForSendingStatus ?? false;
|
|
214
|
+
|
|
215
|
+
if (existing.VerifiedForSendingStatus !== true) {
|
|
216
|
+
console.error('Not validated @' + organization.id);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (existing.VerifiedForSendingStatus !== true && existing.DkimAttributes?.Status === 'FAILED') {
|
|
220
|
+
console.error('AWS failed to verify DKIM records. Triggering a forced recheck @' + organization.id);
|
|
221
|
+
|
|
222
|
+
const deleteCmd = new DeleteEmailIdentityCommand({
|
|
223
|
+
EmailIdentity: organization.privateMeta.mailDomain,
|
|
224
|
+
});
|
|
225
|
+
await client.send(deleteCmd);
|
|
226
|
+
|
|
227
|
+
// Recreate it immediately
|
|
228
|
+
exists = false;
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.error(e);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!exists) {
|
|
235
|
+
console.log('Creating email identity in AWS SES...');
|
|
236
|
+
|
|
237
|
+
const cmd = new CreateEmailIdentityCommand({
|
|
238
|
+
EmailIdentity: organization.privateMeta.mailDomain,
|
|
239
|
+
ConfigurationSetName: expectedConfigurationSetName,
|
|
240
|
+
DkimSigningAttributes: {
|
|
241
|
+
DomainSigningPrivateKey: organization.serverMeta.privateDKIMKey!,
|
|
242
|
+
DomainSigningSelector: Formatter.slug(STAMHOOFD.platformName),
|
|
243
|
+
},
|
|
244
|
+
Tags: [
|
|
245
|
+
{
|
|
246
|
+
Key: 'OrganizationId',
|
|
247
|
+
Value: organization.id,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
Key: 'Environment',
|
|
251
|
+
Value: STAMHOOFD.environment ?? 'Unknown',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await client.send(cmd);
|
|
257
|
+
organization.privateMeta.mailDomainActive = result.VerifiedForSendingStatus ?? false;
|
|
258
|
+
|
|
259
|
+
// Disable email forwarding of bounces and complaints
|
|
260
|
+
// We handle this now with the configuration set
|
|
261
|
+
const putFeedbackCmd = new PutEmailIdentityFeedbackAttributesCommand({
|
|
262
|
+
EmailIdentity: organization.privateMeta.mailDomain,
|
|
263
|
+
EmailForwardingEnabled: false,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await client.send(putFeedbackCmd);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (organization.privateMeta.mailFromDomain && (!exists || (existing && (!existing.MailFromAttributes || existing.MailFromAttributes.MailFromDomain !== organization.privateMeta.mailFromDomain)))) {
|
|
270
|
+
// Also set a from domain, to fix SPF
|
|
271
|
+
console.log('Setting mail from domain: ' + organization.privateMeta.mailFromDomain + ' for ' + organization.id);
|
|
272
|
+
|
|
273
|
+
const cmd = new PutEmailIdentityMailFromAttributesCommand({
|
|
274
|
+
EmailIdentity: organization.privateMeta.mailDomain,
|
|
275
|
+
BehaviorOnMxFailure: 'USE_DEFAULT_VALUE',
|
|
276
|
+
MailFromDomain: organization.privateMeta.mailFromDomain,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await client.send(cmd);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EmailMocker } from '@stamhoofd/email';
|
|
2
|
+
import type { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { EmailTemplateFactory, OrganizationFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { EmailTemplateType, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
5
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
|
+
import { OrganizationEmailService } from './OrganizationEmailService.js';
|
|
7
|
+
|
|
8
|
+
async function createOrganizationWithAdmin(): Promise<Organization> {
|
|
9
|
+
const organization = await new OrganizationFactory({}).create();
|
|
10
|
+
await new UserFactory({
|
|
11
|
+
organization,
|
|
12
|
+
email: 'admin@example.com',
|
|
13
|
+
firstName: 'Ad',
|
|
14
|
+
lastName: 'Min',
|
|
15
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
16
|
+
}).create();
|
|
17
|
+
return organization;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setupEnvironment() {
|
|
21
|
+
TestUtils.setEnvironment('userMode', 'organization');
|
|
22
|
+
|
|
23
|
+
// The notification emails localize the marketing domain, which the shared test environment does not set
|
|
24
|
+
TestUtils.setEnvironment('domains', {
|
|
25
|
+
...STAMHOOFD.domains,
|
|
26
|
+
marketing: { '': 'stamhoofd.dev', BE: 'be.stamhoofd.dev', NL: 'nl.stamhoofd.dev' },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('OrganizationEmailService.checkDrips', () => {
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
await new EmailTemplateFactory({
|
|
33
|
+
type: EmailTemplateType.OrganizationDripWelcome,
|
|
34
|
+
subject: EmailTemplateType.OrganizationDripWelcome,
|
|
35
|
+
html: '<p>{{organizationName}} - {{mailDomain}}</p>',
|
|
36
|
+
text: '{{organizationName}} - {{mailDomain}}',
|
|
37
|
+
}).create();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
setupEnvironment();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Sends the welcome drip email only once', async () => {
|
|
45
|
+
const organization = await createOrganizationWithAdmin();
|
|
46
|
+
|
|
47
|
+
await OrganizationEmailService.checkDrips(organization);
|
|
48
|
+
|
|
49
|
+
const emails = await EmailMocker.transactional.getSucceededEmails();
|
|
50
|
+
expect(emails).toHaveLength(1);
|
|
51
|
+
expect(emails[0].subject).toBe(EmailTemplateType.OrganizationDripWelcome);
|
|
52
|
+
expect(organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWelcome)).toBe(true);
|
|
53
|
+
|
|
54
|
+
await OrganizationEmailService.checkDrips(organization);
|
|
55
|
+
expect(await EmailMocker.transactional.getSucceededEmails()).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { I18n } from '@stamhoofd/backend-i18n';
|
|
2
|
+
import type { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
|
|
4
|
+
import { EmailTemplateType, Replacement, STPackageType } from '@stamhoofd/structures';
|
|
5
|
+
import { Country } from '@stamhoofd/types/Country';
|
|
6
|
+
import { Language } from '@stamhoofd/types/Language';
|
|
7
|
+
import { OrganizationAdminService } from './OrganizationAdminService.js';
|
|
8
|
+
|
|
9
|
+
export class OrganizationEmailService {
|
|
10
|
+
static async sendEmailTemplate(organization: Organization, data: {
|
|
11
|
+
type: EmailTemplateType;
|
|
12
|
+
personal?: boolean;
|
|
13
|
+
bcc?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const recipients = await OrganizationAdminService.getAdminRecipients(organization);
|
|
16
|
+
const defaultI18n = new I18n(Language.Dutch, Country.Belgium);
|
|
17
|
+
const i18n = organization.i18n;
|
|
18
|
+
|
|
19
|
+
const replaceAll = [
|
|
20
|
+
{
|
|
21
|
+
from: defaultI18n.localizedDomains.marketing(),
|
|
22
|
+
to: i18n.localizedDomains.marketing(),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
from: defaultI18n.$t('%2a'),
|
|
26
|
+
to: i18n.$t('%2a'),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
from: defaultI18n.$t('%19'),
|
|
30
|
+
to: i18n.$t('%19'),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
await sendEmailTemplate(null, {
|
|
35
|
+
replaceAll,
|
|
36
|
+
recipients,
|
|
37
|
+
template: {
|
|
38
|
+
type: data.type,
|
|
39
|
+
},
|
|
40
|
+
singleBcc: data.bcc ? { email: 'simon@stamhoofd.be' } : undefined,
|
|
41
|
+
type: 'transactional',
|
|
42
|
+
defaultReplacements: [
|
|
43
|
+
Replacement.create({
|
|
44
|
+
token: 'mailDomain',
|
|
45
|
+
value: organization.privateMeta.mailDomain ?? organization.privateMeta.pendingMailDomain ?? '',
|
|
46
|
+
}),
|
|
47
|
+
Replacement.create({
|
|
48
|
+
token: 'organizationName',
|
|
49
|
+
value: organization.name,
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
unsubscribeType: 'marketing',
|
|
53
|
+
fromStamhoofd: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static async checkDrips(organization: Organization) {
|
|
58
|
+
const days7 = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
59
|
+
|
|
60
|
+
// Welcome drip
|
|
61
|
+
// Created maximum 7 days ago
|
|
62
|
+
if (organization.createdAt > days7 && !organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWelcome)) {
|
|
63
|
+
await this.sendEmailTemplate(organization, {
|
|
64
|
+
type: EmailTemplateType.OrganizationDripWelcome,
|
|
65
|
+
personal: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWelcome);
|
|
69
|
+
await organization.save();
|
|
70
|
+
|
|
71
|
+
return; // Never send more than 1 drip email on the same day
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Webshop trial checkin
|
|
75
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWebshopTrialCheckin)) {
|
|
76
|
+
if (organization.meta.packages.isWebshopsTrial) {
|
|
77
|
+
const activeTime = organization.meta.packages.getActiveTime(STPackageType.TrialWebshops);
|
|
78
|
+
if (activeTime !== null && activeTime > 4 * 24 * 60 * 60 * 1000) {
|
|
79
|
+
// 7 days checkin
|
|
80
|
+
await this.sendEmailTemplate(organization, {
|
|
81
|
+
type: EmailTemplateType.OrganizationDripWebshopTrialCheckin,
|
|
82
|
+
personal: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWebshopTrialCheckin);
|
|
86
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripMembersTrialCheckin); // also mark members checkin
|
|
87
|
+
await organization.save();
|
|
88
|
+
|
|
89
|
+
return; // Never send more than 1 drip email on the same day
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Members trial checkin
|
|
95
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripMembersTrialCheckin)) {
|
|
96
|
+
if (organization.meta.packages.isMembersTrial) {
|
|
97
|
+
const activeTime = organization.meta.packages.getActiveTime(STPackageType.TrialMembers);
|
|
98
|
+
if (activeTime !== null && activeTime > 4 * 24 * 60 * 60 * 1000) {
|
|
99
|
+
// 7 days checkin
|
|
100
|
+
await this.sendEmailTemplate(organization, {
|
|
101
|
+
type: EmailTemplateType.OrganizationDripMembersTrialCheckin,
|
|
102
|
+
personal: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripMembersTrialCheckin);
|
|
106
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWebshopTrialCheckin); // Also mark webshop trial checkin
|
|
107
|
+
await organization.save();
|
|
108
|
+
|
|
109
|
+
return; // Never send more than 1 drip email on the same day
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Webshop trial expired after 1 week
|
|
115
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWebshopTrialExpired)) {
|
|
116
|
+
if (!organization.meta.packages.useWebshops) {
|
|
117
|
+
const deactivatedTime = organization.meta.packages.getDeactivatedTime(STPackageType.TrialWebshops);
|
|
118
|
+
if (deactivatedTime !== null && deactivatedTime < 14 * 24 * 60 * 60 * 1000 && deactivatedTime > 7 * 24 * 60 * 60 * 1000) {
|
|
119
|
+
await this.sendEmailTemplate(organization, {
|
|
120
|
+
type: EmailTemplateType.OrganizationDripWebshopTrialExpired,
|
|
121
|
+
personal: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWebshopTrialExpired);
|
|
125
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripMembersTrialExpired); // also mark members
|
|
126
|
+
await organization.save();
|
|
127
|
+
|
|
128
|
+
return; // Never send more than 1 drip email on the same day
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripMembersTrialExpired)) {
|
|
134
|
+
if (!organization.meta.packages.useMembers) {
|
|
135
|
+
const deactivatedTime = organization.meta.packages.getDeactivatedTime(STPackageType.TrialMembers);
|
|
136
|
+
if (deactivatedTime !== null && deactivatedTime < 14 * 24 * 60 * 60 * 1000 && deactivatedTime > 7 * 24 * 60 * 60 * 1000) {
|
|
137
|
+
await this.sendEmailTemplate(organization, {
|
|
138
|
+
type: EmailTemplateType.OrganizationDripMembersTrialExpired,
|
|
139
|
+
personal: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripMembersTrialExpired);
|
|
143
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWebshopTrialExpired); // also mark webshops
|
|
144
|
+
await organization.save();
|
|
145
|
+
|
|
146
|
+
return; // Never send more than 1 drip email on the same day
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// trial expired reminder (after 10 months)
|
|
152
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripTrialExpiredReminder)) {
|
|
153
|
+
if (!organization.meta.packages.isPaid && !organization.meta.packages.wasPaid) {
|
|
154
|
+
const deactivatedTime1 = organization.meta.packages.getDeactivatedTime(STPackageType.TrialWebshops);
|
|
155
|
+
const deactivatedTime2 = organization.meta.packages.getDeactivatedTime(STPackageType.TrialMembers);
|
|
156
|
+
|
|
157
|
+
const deactivatedTime = deactivatedTime1 && deactivatedTime2 ? Math.max(deactivatedTime1, deactivatedTime2) : (deactivatedTime1 ? deactivatedTime1 : deactivatedTime2);
|
|
158
|
+
|
|
159
|
+
if (deactivatedTime !== null && deactivatedTime > 10 * 30 * 24 * 60 * 60 * 1000 && deactivatedTime < 13 * 31 * 24 * 60 * 60 * 1000) {
|
|
160
|
+
await this.sendEmailTemplate(organization, {
|
|
161
|
+
type: EmailTemplateType.OrganizationDripTrialExpiredReminder,
|
|
162
|
+
personal: true,
|
|
163
|
+
bcc: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripTrialExpiredReminder);
|
|
167
|
+
await organization.save();
|
|
168
|
+
|
|
169
|
+
return; // Never send more than 1 drip email on the same day
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWebshopNotRenewed)) {
|
|
175
|
+
if (!organization.meta.packages.useWebshops) {
|
|
176
|
+
const deactivatedTime = organization.meta.packages.getDeactivatedTime(STPackageType.Webshops);
|
|
177
|
+
|
|
178
|
+
if (deactivatedTime !== null && deactivatedTime > 30 * 24 * 60 * 60 * 1000 && deactivatedTime < 30 * 3 * 24 * 60 * 60 * 1000) {
|
|
179
|
+
await this.sendEmailTemplate(organization, {
|
|
180
|
+
type: EmailTemplateType.OrganizationDripWebshopNotRenewed,
|
|
181
|
+
personal: true,
|
|
182
|
+
bcc: true,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripWebshopNotRenewed);
|
|
186
|
+
await organization.save();
|
|
187
|
+
|
|
188
|
+
return; // Never send more than 1 drip email on the same day
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripMembersNotRenewed)) {
|
|
194
|
+
if (!organization.meta.packages.useMembers) {
|
|
195
|
+
const deactivatedTime = organization.meta.packages.getDeactivatedTime(STPackageType.Members);
|
|
196
|
+
|
|
197
|
+
if (deactivatedTime !== null && deactivatedTime > 30 * 24 * 60 * 60 * 1000 && deactivatedTime < 30 * 3 * 24 * 60 * 60 * 1000) {
|
|
198
|
+
await this.sendEmailTemplate(organization, {
|
|
199
|
+
type: EmailTemplateType.OrganizationDripMembersNotRenewed,
|
|
200
|
+
personal: true,
|
|
201
|
+
bcc: true,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
organization.serverMeta.addEmail(EmailTemplateType.OrganizationDripMembersNotRenewed);
|
|
205
|
+
await organization.save();
|
|
206
|
+
|
|
207
|
+
return; // Never send more than 1 drip email on the same day
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { I18n } from '@stamhoofd/backend-i18n';
|
|
2
|
+
import { Organization, OrganizationFactory, PasswordToken, User, UserFactory } from '@stamhoofd/models';
|
|
3
|
+
import { Address, OrganizationMetaData, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
4
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
5
|
+
import { Country } from '@stamhoofd/types/Country';
|
|
6
|
+
import { PasswordForgotService } from './PasswordForgotService.js';
|
|
7
|
+
|
|
8
|
+
const i18n = new I18n(I18n.defaultLanguage, I18n.defaultCountry);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An organization that is not saved in the database: enough to build an url with.
|
|
12
|
+
*/
|
|
13
|
+
function buildOrganization(id = 'organization-1') {
|
|
14
|
+
const organization = new Organization();
|
|
15
|
+
organization.id = id;
|
|
16
|
+
organization.name = 'Test organization';
|
|
17
|
+
organization.uri = 'test-organization';
|
|
18
|
+
organization.meta = OrganizationMetaData.create({});
|
|
19
|
+
organization.address = Address.create({
|
|
20
|
+
street: 'Demostraat',
|
|
21
|
+
number: '12',
|
|
22
|
+
city: 'Gent',
|
|
23
|
+
postalCode: '9000',
|
|
24
|
+
country: Country.Belgium,
|
|
25
|
+
});
|
|
26
|
+
return organization;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildUser(organizationId: string | null) {
|
|
30
|
+
const user = new User();
|
|
31
|
+
user.id = 'user-1';
|
|
32
|
+
user.organizationId = organizationId;
|
|
33
|
+
user.email = 'user@example.com';
|
|
34
|
+
return user;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildToken(user: User, token: string) {
|
|
38
|
+
const passwordToken = new PasswordToken();
|
|
39
|
+
passwordToken.token = token;
|
|
40
|
+
passwordToken.userId = user.id;
|
|
41
|
+
return passwordToken;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('PasswordForgotService', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
TestUtils.setEnvironment('userMode', 'organization');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getPasswordRecoveryUrl', () => {
|
|
50
|
+
test('An administrator of the organization gets a dashboard url', async () => {
|
|
51
|
+
const organization = await new OrganizationFactory({}).create();
|
|
52
|
+
const admin = await new UserFactory({
|
|
53
|
+
organization,
|
|
54
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
55
|
+
}).create();
|
|
56
|
+
|
|
57
|
+
const url = new URL(await PasswordForgotService.getPasswordRecoveryUrl(admin, organization, i18n));
|
|
58
|
+
|
|
59
|
+
expect(url.host).toBe(STAMHOOFD.domains.dashboard);
|
|
60
|
+
expect(url.pathname).toMatch(/\/reset-password$/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('A user without permissions for the organization gets a registration url', async () => {
|
|
64
|
+
const organization = await new OrganizationFactory({}).create();
|
|
65
|
+
const member = await new UserFactory({ organization }).create();
|
|
66
|
+
|
|
67
|
+
const url = new URL(await PasswordForgotService.getPasswordRecoveryUrl(member, organization, i18n));
|
|
68
|
+
|
|
69
|
+
expect(url.host).not.toBe(STAMHOOFD.domains.dashboard);
|
|
70
|
+
expect(url.host.split('.')[0]).toBe(organization.uri);
|
|
71
|
+
expect(url.pathname).toMatch(/\/reset-password$/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('The url points to the reset-password page with a url encoded token', async () => {
|
|
75
|
+
const organization = buildOrganization();
|
|
76
|
+
const user = buildUser(organization.id);
|
|
77
|
+
|
|
78
|
+
const url = await PasswordForgotService.getPasswordRecoveryUrlForToken(buildToken(user, 'a+b/c=d&e'), organization, i18n, user);
|
|
79
|
+
|
|
80
|
+
expect(url).toMatch(/\/reset-password\?token=a%2Bb%2Fc%3Dd%26e$/);
|
|
81
|
+
expect(new URL(url).searchParams.get('token')).toBe('a+b/c=d&e');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('Throws when the user belongs to a different organization', async () => {
|
|
85
|
+
const user = buildUser('a-different-organization');
|
|
86
|
+
|
|
87
|
+
await expect(PasswordForgotService.getPasswordRecoveryUrlForToken(buildToken(user, 'my-token'), buildOrganization(), i18n, user))
|
|
88
|
+
.rejects
|
|
89
|
+
.toThrow('Unexpected mismatch in organization id for PasswordToken');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Allows a user without an organization in every scope', async () => {
|
|
93
|
+
const user = buildUser(null);
|
|
94
|
+
|
|
95
|
+
await expect(PasswordForgotService.getPasswordRecoveryUrlForToken(buildToken(user, 'my-token'), buildOrganization(), i18n, user))
|
|
96
|
+
.toResolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|