@stamhoofd/backend 2.80.1 → 2.82.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/index.ts +1 -4
- package/package.json +10 -10
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +139 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +28 -5
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +16 -35
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -8
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -1
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +68 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -7
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +106 -0
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +16 -3
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +247 -0
- package/src/endpoints/{auth → organization/dashboard/users}/PatchApiUserEndpoint.ts +24 -5
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +5 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -1
- package/src/excel-loaders/event-notifications.ts +133 -0
- package/src/excel-loaders/index.ts +5 -0
- package/src/excel-loaders/members.ts +15 -38
- package/src/excel-loaders/organizations.ts +2 -2
- package/src/excel-loaders/payments.ts +2 -2
- package/src/helpers/AuthenticatedStructures.ts +8 -0
- package/src/helpers/CheckSettlements.ts +1 -1
- package/src/helpers/Context.ts +28 -12
- package/src/helpers/StripeHelper.ts +12 -1
- package/src/helpers/{xlsxAddressTransformerColumnFactory.ts → XlsxTransformerColumnHelper.ts} +68 -2
- package/tests/e2e/api-rate-limits.test.ts +188 -0
- package/tests/helpers/StripeMocker.ts +7 -1
- /package/src/endpoints/global/platform/{GetPlatformEnpoint.ts → GetPlatformEndpoint.ts} +0 -0
|
@@ -6,7 +6,7 @@ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEn
|
|
|
6
6
|
import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint';
|
|
7
7
|
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../helpers/Context';
|
|
9
|
-
import { XlsxTransformerColumnHelper } from '../helpers/
|
|
9
|
+
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
|
|
10
10
|
|
|
11
11
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
12
12
|
const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
@@ -16,7 +16,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
16
16
|
{
|
|
17
17
|
id: 'id',
|
|
18
18
|
name: 'ID',
|
|
19
|
-
width:
|
|
19
|
+
width: 40,
|
|
20
20
|
getValue: ({ patchedMember: object }: PlatformMember) => ({
|
|
21
21
|
value: object.id,
|
|
22
22
|
}),
|
|
@@ -89,7 +89,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
89
89
|
{
|
|
90
90
|
id: 'email',
|
|
91
91
|
name: 'E-mailadres',
|
|
92
|
-
width:
|
|
92
|
+
width: 40,
|
|
93
93
|
getValue: ({ patchedMember: object }: PlatformMember) => ({
|
|
94
94
|
value: object.details.email,
|
|
95
95
|
}),
|
|
@@ -247,42 +247,19 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
247
247
|
},
|
|
248
248
|
|
|
249
249
|
// Dynamic records
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const recordSettingId = id.split('.')[1];
|
|
262
|
-
console.log('recordSettingId', recordSettingId);
|
|
263
|
-
const recordSetting = recordSettings.find(r => r.id === recordSettingId);
|
|
264
|
-
|
|
265
|
-
if (!recordSetting) {
|
|
266
|
-
// Will throw a proper error itself
|
|
267
|
-
console.log('recordSetting not found', recordSettings);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const columns = recordSetting.excelColumns;
|
|
272
|
-
|
|
273
|
-
return columns.map((columnName, index) => {
|
|
274
|
-
return {
|
|
275
|
-
id: `recordAnswers.${recordSettingId}.${index}`,
|
|
276
|
-
name: columnName,
|
|
277
|
-
width: 20,
|
|
278
|
-
getValue: ({ patchedMember: object }: PlatformMember) => ({
|
|
279
|
-
value: object.details.recordAnswers.get(recordSettingId)?.excelValues[index]?.value ?? '',
|
|
280
|
-
}),
|
|
281
|
-
};
|
|
282
|
-
});
|
|
283
|
-
}
|
|
250
|
+
XlsxTransformerColumnHelper.createRecordAnswersColumns({
|
|
251
|
+
matchId: 'recordAnswers',
|
|
252
|
+
getRecordAnswers: ({ patchedMember: object }: PlatformMember) => object.details.recordAnswers,
|
|
253
|
+
getRecordCategories: () => {
|
|
254
|
+
const platform = PlatformStruct.shared;
|
|
255
|
+
const organization = Context.organization;
|
|
256
|
+
|
|
257
|
+
return [
|
|
258
|
+
...(organization?.meta.recordsConfiguration.recordCategories ?? []),
|
|
259
|
+
...platform.config.recordsConfiguration.recordCategories,
|
|
260
|
+
];
|
|
284
261
|
},
|
|
285
|
-
},
|
|
262
|
+
}),
|
|
286
263
|
|
|
287
264
|
// Registration records
|
|
288
265
|
{
|
|
@@ -2,7 +2,7 @@ import { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
|
2
2
|
import { Platform as PlatformStruct, ExcelExportType, LimitedFilteredRequest, Organization as OrganizationStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, PaginatedResponse, MemberWithRegistrationsBlob, Premise } from '@stamhoofd/structures';
|
|
3
3
|
import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint';
|
|
4
4
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
|
|
5
|
-
import { XlsxTransformerColumnHelper } from '../helpers/
|
|
5
|
+
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
|
|
6
6
|
import { Group, Member, MemberResponsibilityRecord } from '@stamhoofd/models';
|
|
7
7
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
8
8
|
import { ArrayDecoder, field } from '@simonbackx/simple-encoding';
|
|
@@ -33,7 +33,7 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
|
|
|
33
33
|
{
|
|
34
34
|
id: 'id',
|
|
35
35
|
name: 'ID',
|
|
36
|
-
width:
|
|
36
|
+
width: 40,
|
|
37
37
|
getValue: (object: Object) => ({
|
|
38
38
|
value: object.id,
|
|
39
39
|
}),
|
|
@@ -5,7 +5,7 @@ import { BalanceItemPaymentDetailed, BalanceItemRelationType, ExcelExportType, g
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
|
|
7
7
|
import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint';
|
|
8
|
-
import { XlsxTransformerColumnHelper } from '../helpers/
|
|
8
|
+
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
|
|
9
9
|
|
|
10
10
|
type PaymentWithItem = {
|
|
11
11
|
payment: PaymentGeneral;
|
|
@@ -475,7 +475,7 @@ function getPayingOrganizationColumns(): XlsxTransformerColumn<PaymentGeneral>[]
|
|
|
475
475
|
{
|
|
476
476
|
id: 'payingOrganization.id',
|
|
477
477
|
name: 'ID betalende groep',
|
|
478
|
-
width:
|
|
478
|
+
width: 40,
|
|
479
479
|
getValue: (object: PaymentGeneralWithStripeAccount) => {
|
|
480
480
|
return {
|
|
481
481
|
value: object.payingOrganization?.id || '',
|
|
@@ -423,6 +423,14 @@ export class AuthenticatedStructures {
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
if (includeContextOrganization && STAMHOOFD.singleOrganization && !Context.auth.organization) {
|
|
427
|
+
const found = organizations.get(STAMHOOFD.singleOrganization);
|
|
428
|
+
if (!found) {
|
|
429
|
+
const organization = await Context.auth.getOrganization(STAMHOOFD.singleOrganization);
|
|
430
|
+
organizations.set(organization.id, organization);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
426
434
|
const memberBlobs: MemberWithRegistrationsBlob[] = [];
|
|
427
435
|
for (const member of members) {
|
|
428
436
|
for (const registration of member.registrations) {
|
|
@@ -23,7 +23,7 @@ type MolliePaymentJSON = {
|
|
|
23
23
|
let lastSettlementCheck: Date | null = null;
|
|
24
24
|
|
|
25
25
|
export async function checkAllStripePayouts(checkAll = false) {
|
|
26
|
-
if (STAMHOOFD.environment !== 'production') {
|
|
26
|
+
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.STRIPE_SECRET_KEY) {
|
|
27
27
|
console.log('Skip settlement check');
|
|
28
28
|
return;
|
|
29
29
|
}
|
package/src/helpers/Context.ts
CHANGED
|
@@ -4,29 +4,42 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
4
4
|
import { Organization, Platform, RateLimiter, Token, User } from '@stamhoofd/models';
|
|
5
5
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
6
6
|
|
|
7
|
+
import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
8
|
+
import { ApiUserRateLimits } from '@stamhoofd/structures';
|
|
7
9
|
import { AdminPermissionChecker } from './AdminPermissionChecker';
|
|
8
|
-
import { AutoEncoder, field, Decoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
9
10
|
|
|
10
11
|
export const apiUserRateLimiter = new RateLimiter({
|
|
11
12
|
limits: [
|
|
12
13
|
{
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
limit: {
|
|
15
|
+
'': 25, // (5req/s for 5s)
|
|
16
|
+
[ApiUserRateLimits.Medium]: 10 * 5, // (10req/s for 5s)
|
|
17
|
+
[ApiUserRateLimits.High]: 25 * 5, // (100req/s for 5s)
|
|
18
|
+
},
|
|
15
19
|
duration: 5 * 1000,
|
|
16
20
|
},
|
|
17
21
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
limit: {
|
|
23
|
+
'': 120, // max 1req/s during 150s
|
|
24
|
+
[ApiUserRateLimits.Medium]: 240, // (2req/s for 150s)
|
|
25
|
+
[ApiUserRateLimits.High]: 480, // (4req/s for 150s)
|
|
26
|
+
},
|
|
27
|
+
duration: 120 * 1000,
|
|
21
28
|
},
|
|
22
29
|
{
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
limit: {
|
|
31
|
+
'': 1000, // ± 0.27 request/s sustained for an hour = 3.6s between each request
|
|
32
|
+
[ApiUserRateLimits.Medium]: 2000, // ± 0.56 request/s sustained for an hour
|
|
33
|
+
[ApiUserRateLimits.High]: 4000, // ± 1.11 request/s sustained for an hour
|
|
34
|
+
},
|
|
25
35
|
duration: 60 * 1000 * 60,
|
|
26
36
|
},
|
|
27
37
|
{
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
limit: {
|
|
39
|
+
'': 2_000, // max 2000 requests per day
|
|
40
|
+
[ApiUserRateLimits.Medium]: 14_400, // max 4000 requests per day
|
|
41
|
+
[ApiUserRateLimits.High]: 18_000, // max 10 requests per minute, sustained for a full day
|
|
42
|
+
},
|
|
30
43
|
duration: 24 * 60 * 1000 * 60,
|
|
31
44
|
},
|
|
32
45
|
],
|
|
@@ -169,7 +182,10 @@ export class ContextInstance {
|
|
|
169
182
|
return await this.authenticate({ allowWithoutAccount });
|
|
170
183
|
}
|
|
171
184
|
catch (e) {
|
|
172
|
-
|
|
185
|
+
if (e.code === 'not_authenticated') {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
throw e;
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
191
|
|
|
@@ -235,7 +251,7 @@ export class ContextInstance {
|
|
|
235
251
|
|
|
236
252
|
// Rate limits for api users
|
|
237
253
|
if (token.user.isApiUser) {
|
|
238
|
-
apiUserRateLimiter.track(this.organization?.id ?? token.user.id);
|
|
254
|
+
apiUserRateLimiter.track(this.organization?.id ?? token.user.id, 1, token.user.meta?.rateLimits ?? undefined);
|
|
239
255
|
}
|
|
240
256
|
|
|
241
257
|
const user = token.user;
|
|
@@ -6,7 +6,18 @@ import { Formatter } from '@stamhoofd/utility';
|
|
|
6
6
|
import Stripe from 'stripe';
|
|
7
7
|
|
|
8
8
|
export class StripeHelper {
|
|
9
|
+
static get notConfiguredError() {
|
|
10
|
+
return new SimpleError({
|
|
11
|
+
code: 'not_configured',
|
|
12
|
+
message: 'Stripe is not yet configured for this platform',
|
|
13
|
+
human: $t('Stripe is nog niet geconfigureerd voor dit platform'),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
static getInstance(accountId: string | null = null) {
|
|
18
|
+
if (!STAMHOOFD.STRIPE_SECRET_KEY) {
|
|
19
|
+
throw this.notConfiguredError;
|
|
20
|
+
}
|
|
10
21
|
return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20', typescript: true, maxNetworkRetries: 0, timeout: 10000, stripeAccount: accountId ?? undefined });
|
|
11
22
|
}
|
|
12
23
|
|
|
@@ -81,7 +92,7 @@ export class StripeHelper {
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
static async getStatus(payment: Payment, cancel = false, testMode = false): Promise<PaymentStatus> {
|
|
84
|
-
if (testMode && !STAMHOOFD.STRIPE_SECRET_KEY
|
|
95
|
+
if (testMode && !STAMHOOFD.STRIPE_SECRET_KEY?.startsWith('sk_test_')) {
|
|
85
96
|
// Do not query anything
|
|
86
97
|
return payment.status;
|
|
87
98
|
}
|
package/src/helpers/{xlsxAddressTransformerColumnFactory.ts → XlsxTransformerColumnHelper.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XlsxTransformerColumn } from '@stamhoofd/excel-writer';
|
|
2
|
-
import { Address, CountryHelper, Parent, ParentTypeHelper, PlatformMember } from '@stamhoofd/structures';
|
|
2
|
+
import { Address, CountryHelper, Parent, ParentTypeHelper, PlatformMember, RecordAnswer, RecordCategory, RecordSettings, RecordType } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
export class XlsxTransformerColumnHelper {
|
|
5
5
|
static formatBoolean(value: boolean | undefined | null): string {
|
|
@@ -115,7 +115,8 @@ export class XlsxTransformerColumnHelper {
|
|
|
115
115
|
{
|
|
116
116
|
id: getId('street'),
|
|
117
117
|
name: `Straat`,
|
|
118
|
-
|
|
118
|
+
defaultCategory: 'Adres', // Ignore this name
|
|
119
|
+
width: 40,
|
|
119
120
|
getValue: (object: T) => {
|
|
120
121
|
const address = getAddress(object);
|
|
121
122
|
return {
|
|
@@ -126,6 +127,7 @@ export class XlsxTransformerColumnHelper {
|
|
|
126
127
|
{
|
|
127
128
|
id: getId('number'),
|
|
128
129
|
name: 'Nummer',
|
|
130
|
+
defaultCategory: 'Adres', // Ignore this name
|
|
129
131
|
width: 20,
|
|
130
132
|
getValue: (object: T) => {
|
|
131
133
|
const address = getAddress(object);
|
|
@@ -137,6 +139,7 @@ export class XlsxTransformerColumnHelper {
|
|
|
137
139
|
{
|
|
138
140
|
id: getId('postalCode'),
|
|
139
141
|
name: 'Postcode',
|
|
142
|
+
defaultCategory: 'Adres', // Ignore this name
|
|
140
143
|
width: 20,
|
|
141
144
|
getValue: (object: T) => {
|
|
142
145
|
const address = getAddress(object);
|
|
@@ -148,6 +151,7 @@ export class XlsxTransformerColumnHelper {
|
|
|
148
151
|
{
|
|
149
152
|
id: getId('city'),
|
|
150
153
|
name: 'Stad',
|
|
154
|
+
defaultCategory: 'Adres', // Ignore this name
|
|
151
155
|
width: 20,
|
|
152
156
|
getValue: (object: T) => {
|
|
153
157
|
const address = getAddress(object);
|
|
@@ -159,6 +163,7 @@ export class XlsxTransformerColumnHelper {
|
|
|
159
163
|
{
|
|
160
164
|
id: getId('country'),
|
|
161
165
|
name: 'Land',
|
|
166
|
+
defaultCategory: 'Adres', // Ignore this name
|
|
162
167
|
width: 20,
|
|
163
168
|
getValue: (object: T) => {
|
|
164
169
|
const address = getAddress(object);
|
|
@@ -173,4 +178,65 @@ export class XlsxTransformerColumnHelper {
|
|
|
173
178
|
},
|
|
174
179
|
};
|
|
175
180
|
}
|
|
181
|
+
|
|
182
|
+
static createRecordAnswersColumns<T>({ matchId, getRecordCategories, getRecordAnswers }: { matchId: string; getRecordCategories: () => RecordCategory[]; getRecordAnswers: (object: T) => Map<string, RecordAnswer> }): XlsxTransformerColumn<T> {
|
|
183
|
+
return {
|
|
184
|
+
match(id) {
|
|
185
|
+
if (id.startsWith(matchId + '.')) {
|
|
186
|
+
const recordCategories = getRecordCategories();
|
|
187
|
+
const flattenedCategories = RecordCategory.flattenCategoriesWith(recordCategories, r => r.excelColumns.length > 0);
|
|
188
|
+
|
|
189
|
+
let recordCategory: RecordCategory | undefined;
|
|
190
|
+
let recordSetting: RecordSettings | undefined;
|
|
191
|
+
const recordSettingId = id.split('.')[1];
|
|
192
|
+
|
|
193
|
+
for (const category of flattenedCategories) {
|
|
194
|
+
const recordSettings = category.getAllRecords();
|
|
195
|
+
const rr = recordSettings.find(r => r.id === recordSettingId);
|
|
196
|
+
|
|
197
|
+
if (rr) {
|
|
198
|
+
recordSetting = rr;
|
|
199
|
+
recordCategory = category;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!recordSetting || !recordCategory) {
|
|
205
|
+
// Will throw a proper error itself
|
|
206
|
+
console.log('recordSetting not found');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const columns = recordSetting.excelColumns;
|
|
211
|
+
|
|
212
|
+
return columns.map(({ name, width, defaultCategory }, index) => {
|
|
213
|
+
return {
|
|
214
|
+
id: `${matchId}.${recordSettingId}.${index}`,
|
|
215
|
+
name,
|
|
216
|
+
width: width ?? 20,
|
|
217
|
+
defaultCategory,
|
|
218
|
+
category: recordCategory.name,
|
|
219
|
+
getValue: (object: T) => {
|
|
220
|
+
const answers = getRecordAnswers(object);
|
|
221
|
+
const b = (answers.get(recordSettingId)?.excelValues[index] ?? {
|
|
222
|
+
value: '',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...b,
|
|
227
|
+
style: {
|
|
228
|
+
...b.style,
|
|
229
|
+
alignment: {
|
|
230
|
+
...b.style?.alignment,
|
|
231
|
+
wrapText: recordSetting.type === RecordType.Textarea,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
176
242
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable jest/no-conditional-expect */
|
|
2
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { Organization, OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
import { PatchMap } from '@simonbackx/simple-encoding';
|
|
6
|
+
import { ApiUser, ApiUserRateLimits, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserMeta, UserPermissions } from '@stamhoofd/structures';
|
|
7
|
+
import { SHExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
8
|
+
import { CreateApiUserEndpoint } from '../../src/endpoints/organization/dashboard/users/CreateApiUserEndpoint';
|
|
9
|
+
import { testServer } from '../helpers/TestServer';
|
|
10
|
+
import { GetUserEndpoint } from '../../src/endpoints/auth/GetUserEndpoint';
|
|
11
|
+
|
|
12
|
+
describe('E2E.APIRateLimits', () => {
|
|
13
|
+
// Test endpoint
|
|
14
|
+
const createEndpoint = new CreateApiUserEndpoint();
|
|
15
|
+
const getUserEndpoint = new GetUserEndpoint();
|
|
16
|
+
let organization: Organization;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
20
|
+
organization = await new OrganizationFactory({}).create();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Note: we don't use a factory because this is an E2E test and
|
|
25
|
+
* we also want to check if the created tokens with the API are actually marked as API-keys and not normal users.
|
|
26
|
+
*/
|
|
27
|
+
async function createAPIToken(rateLimits: ApiUserRateLimits | null) {
|
|
28
|
+
const user = await new UserFactory({
|
|
29
|
+
globalPermissions: Permissions.create({
|
|
30
|
+
level: PermissionLevel.Full,
|
|
31
|
+
}),
|
|
32
|
+
}).create();
|
|
33
|
+
const token = await Token.createToken(user);
|
|
34
|
+
|
|
35
|
+
const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
|
|
36
|
+
permissions: UserPermissions.create({
|
|
37
|
+
organizationPermissions: new Map([
|
|
38
|
+
[organization.id, Permissions.create({ level: PermissionLevel.Read })],
|
|
39
|
+
]),
|
|
40
|
+
}),
|
|
41
|
+
meta: UserMeta.create({
|
|
42
|
+
rateLimits,
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
createRequest.headers.authorization = 'Bearer ' + token.accessToken;
|
|
46
|
+
const createResponse = await testServer.test(createEndpoint, createRequest);
|
|
47
|
+
|
|
48
|
+
expect(createResponse.body.meta?.rateLimits ?? undefined).toEqual(rateLimits ?? undefined);
|
|
49
|
+
|
|
50
|
+
return createResponse.body.token;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('By default throws after 25 requests in less than 5s', async () => {
|
|
54
|
+
const token = await createAPIToken(null);
|
|
55
|
+
|
|
56
|
+
// Start firing
|
|
57
|
+
for (let i = 0; i < 30; i++) {
|
|
58
|
+
const request = Request.buildJson('GET', '/user', organization.getApiHost());
|
|
59
|
+
request.headers.authorization = 'Bearer ' + token;
|
|
60
|
+
const promise = testServer.test(getUserEndpoint, request);
|
|
61
|
+
|
|
62
|
+
if (i < 25) {
|
|
63
|
+
try {
|
|
64
|
+
await expect(promise).toResolve();
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
let error: any = null;
|
|
68
|
+
try {
|
|
69
|
+
await promise;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
error = e;
|
|
73
|
+
}
|
|
74
|
+
throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await expect(promise).rejects.toThrow(
|
|
79
|
+
SHExpect.simpleError({
|
|
80
|
+
code: 'rate_limit',
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Normal rate limit throws after 25 requests in less than 5s', async () => {
|
|
88
|
+
const token = await createAPIToken(ApiUserRateLimits.Normal);
|
|
89
|
+
|
|
90
|
+
// Start firing
|
|
91
|
+
for (let i = 0; i < 30; i++) {
|
|
92
|
+
const request = Request.buildJson('GET', '/user', organization.getApiHost());
|
|
93
|
+
request.headers.authorization = 'Bearer ' + token;
|
|
94
|
+
const promise = testServer.test(getUserEndpoint, request);
|
|
95
|
+
|
|
96
|
+
if (i < 25) {
|
|
97
|
+
try {
|
|
98
|
+
await expect(promise).toResolve();
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
let error: any = null;
|
|
102
|
+
try {
|
|
103
|
+
await promise;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
error = e;
|
|
107
|
+
}
|
|
108
|
+
throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await expect(promise).rejects.toThrow(
|
|
113
|
+
SHExpect.simpleError({
|
|
114
|
+
code: 'rate_limit',
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('Medium rate limits throw after 50 requests in less than 5s', async () => {
|
|
122
|
+
const token = await createAPIToken(ApiUserRateLimits.Medium);
|
|
123
|
+
|
|
124
|
+
// Start firing
|
|
125
|
+
for (let i = 0; i < 60; i++) {
|
|
126
|
+
const request = Request.buildJson('GET', '/user', organization.getApiHost());
|
|
127
|
+
request.headers.authorization = 'Bearer ' + token;
|
|
128
|
+
const promise = testServer.test(getUserEndpoint, request);
|
|
129
|
+
|
|
130
|
+
if (i < 50) {
|
|
131
|
+
try {
|
|
132
|
+
await expect(promise).toResolve();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
let error: any = null;
|
|
136
|
+
try {
|
|
137
|
+
await promise;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
error = e;
|
|
141
|
+
}
|
|
142
|
+
throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
await expect(promise).rejects.toThrow(
|
|
147
|
+
SHExpect.simpleError({
|
|
148
|
+
code: 'rate_limit',
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('High rate limits throw after 125 requests in less than 5s', async () => {
|
|
156
|
+
const token = await createAPIToken(ApiUserRateLimits.High);
|
|
157
|
+
|
|
158
|
+
// Start firing
|
|
159
|
+
for (let i = 0; i < 140; i++) {
|
|
160
|
+
const request = Request.buildJson('GET', '/user', organization.getApiHost());
|
|
161
|
+
request.headers.authorization = 'Bearer ' + token;
|
|
162
|
+
const promise = testServer.test(getUserEndpoint, request);
|
|
163
|
+
|
|
164
|
+
if (i < 125) {
|
|
165
|
+
try {
|
|
166
|
+
await expect(promise).toResolve();
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
let error: any = null;
|
|
170
|
+
try {
|
|
171
|
+
await promise;
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
error = e;
|
|
175
|
+
}
|
|
176
|
+
throw new Error('The endpoint rejected at call ' + i + ' with error message ' + error?.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await expect(promise).rejects.toThrow(
|
|
181
|
+
SHExpect.simpleError({
|
|
182
|
+
code: 'rate_limit',
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -165,9 +165,15 @@ export class StripeMocker {
|
|
|
165
165
|
const endpoint = new StripeWebookEndpoint();
|
|
166
166
|
|
|
167
167
|
const r = Request.buildJson('POST', `/stripe/webhooks`, undefined, payload);
|
|
168
|
+
const secret = payload.account ? STAMHOOFD.STRIPE_CONNECT_ENDPOINT_SECRET : STAMHOOFD.STRIPE_ENDPOINT_SECRET;
|
|
169
|
+
|
|
170
|
+
if (!secret) {
|
|
171
|
+
throw new Error('No stripe secret set in env');
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
r.headers['stripe-signature'] = stripe.webhooks.generateTestHeaderString({
|
|
169
175
|
payload: await r.body,
|
|
170
|
-
secret:
|
|
176
|
+
secret: secret,
|
|
171
177
|
});
|
|
172
178
|
await testServer.test(endpoint, r);
|
|
173
179
|
}
|
|
File without changes
|