@stamhoofd/backend 2.83.5 → 2.84.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/index.ts +19 -4
- package/package.json +18 -14
- package/src/crons/amazon-ses.ts +26 -5
- package/src/crons/balance-emails.ts +18 -17
- package/src/email-recipient-loaders/registrations.ts +87 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
- package/src/endpoints/global/files/UploadFile.ts +11 -16
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +233 -232
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +153 -0
- package/src/helpers/AdminPermissionChecker.ts +65 -37
- package/src/helpers/AuthenticatedStructures.ts +43 -3
- package/src/helpers/Context.ts +29 -1
- package/src/helpers/GlobalHelper.ts +3 -1
- package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
- package/src/helpers/GroupedThrottledQueue.ts +108 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
- package/src/helpers/MemberCharger.ts +0 -5
- package/src/helpers/MembershipCharger.ts +3 -9
- package/src/helpers/OrganizationCharger.ts +0 -5
- package/src/helpers/ThrottledQueue.test.ts +194 -0
- package/src/helpers/ThrottledQueue.ts +145 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/services/BalanceItemPaymentService.ts +1 -33
- package/src/services/BalanceItemService.ts +167 -48
- package/src/services/FileSignService.ts +18 -13
- package/src/services/MemberRecordStore.ts +28 -19
- package/src/services/PaymentReallocationService.test.ts +25 -14
- package/src/services/PaymentReallocationService.ts +29 -10
- package/src/services/PaymentService.ts +4 -16
- package/src/services/PlatformMembershipService.ts +8 -4
- package/src/services/RegistrationService.ts +66 -2
- package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
- package/src/sql-filters/groups.ts +67 -0
- package/src/sql-filters/members.ts +33 -58
- package/src/sql-filters/organization-registration-periods.ts +8 -0
- package/src/sql-filters/registration-periods.ts +8 -0
- package/src/sql-filters/registrations.ts +11 -22
- package/src/sql-sorters/groups.ts +24 -0
- package/src/sql-sorters/organization-registration-periods.ts +24 -0
- package/src/sql-sorters/registration-periods.ts +47 -0
- package/src/sql-sorters/registrations.ts +77 -0
- package/tests/actions/patchOrganizationMember.ts +27 -0
- package/tests/actions/patchPaymentStatus.ts +45 -0
- package/tests/actions/patchUserMember.ts +27 -0
- package/tests/assertions/assertBalances.ts +49 -0
- package/tests/e2e/api-rate-limits.test.ts +5 -5
- package/tests/e2e/bundle-discounts.test.ts +4060 -0
- package/tests/e2e/charge-members.test.ts +27 -24
- package/tests/e2e/documents.test.ts +398 -0
- package/tests/e2e/register.test.ts +292 -312
- package/tests/helpers/PayconiqMocker.ts +55 -0
- package/tests/init/index.ts +5 -0
- package/tests/init/initAdmin.ts +14 -0
- package/tests/init/initBundleDiscount.ts +47 -0
- package/tests/init/initPayconiq.ts +9 -0
- package/tests/init/initPlatformAdmin.ts +13 -0
- package/tests/init/initStripe.ts +21 -0
- package/tests/jest.setup.ts +29 -0
- package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Member, Platform } from '@stamhoofd/models';
|
|
4
|
+
import { Group, Member, Platform } from '@stamhoofd/models';
|
|
5
5
|
import { SQL, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
6
|
-
import { CountFilteredRequest, Country, CountryCode, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
6
|
+
import { CountFilteredRequest, Country, CountryCode, GroupType, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
7
|
import { DataValidator } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
@@ -12,6 +12,7 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
12
12
|
import { Context } from '../../../helpers/Context';
|
|
13
13
|
import { memberFilterCompilers } from '../../../sql-filters/members';
|
|
14
14
|
import { memberSorters } from '../../../sql-sorters/members';
|
|
15
|
+
import { validateGroupFilter } from './helpers/validateGroupFilter';
|
|
15
16
|
|
|
16
17
|
type Params = Record<string, never>;
|
|
17
18
|
type Query = LimitedFilteredRequest;
|
|
@@ -41,75 +42,88 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
41
42
|
const organization = Context.organization;
|
|
42
43
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const platform = await Platform.getShared();
|
|
52
|
-
|
|
53
|
-
// Add organization scope filter
|
|
54
|
-
scopeFilter = {
|
|
55
|
-
registrations: {
|
|
56
|
-
$elemMatch: {
|
|
57
|
-
organization: {
|
|
58
|
-
tags: {
|
|
59
|
-
$in: tags,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
periodId: platform.periodId,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (organization && !Context.auth.canAccessAllPlatformMembers()) {
|
|
70
|
-
// Add organization scope filter
|
|
71
|
-
const groups = await Context.auth.getAccessibleGroups(organization.id, permissionLevel);
|
|
45
|
+
// First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
|
|
46
|
+
if (!Context.auth.canAccessAllPlatformMembers() && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
|
|
47
|
+
if (!organization) {
|
|
48
|
+
const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
|
|
49
|
+
if (tags !== 'all' && tags.length === 0) {
|
|
50
|
+
throw Context.auth.error();
|
|
51
|
+
}
|
|
72
52
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
53
|
+
if (tags !== 'all') {
|
|
54
|
+
const platform = await Platform.getShared();
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
79
|
-
// Can access full history for now
|
|
56
|
+
// Add organization scope filter
|
|
80
57
|
scopeFilter = {
|
|
81
58
|
registrations: {
|
|
82
59
|
$elemMatch: {
|
|
83
|
-
|
|
60
|
+
organization: {
|
|
61
|
+
tags: {
|
|
62
|
+
$in: tags,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
periodId: platform.periodId,
|
|
84
66
|
},
|
|
85
67
|
},
|
|
86
68
|
};
|
|
87
69
|
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (organization) {
|
|
73
|
+
// Add organization scope filter
|
|
74
|
+
if (await Context.auth.canAccessAllMembers(organization.id, permissionLevel)) {
|
|
75
|
+
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
76
|
+
// Can access full history for now
|
|
77
|
+
scopeFilter = {
|
|
78
|
+
registrations: {
|
|
79
|
+
$elemMatch: {
|
|
80
|
+
organizationId: organization.id,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Can only access current period
|
|
87
|
+
scopeFilter = {
|
|
88
|
+
registrations: {
|
|
89
|
+
$elemMatch: {
|
|
90
|
+
organizationId: organization.id,
|
|
91
|
+
periodId: organization.periodId,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
88
97
|
else {
|
|
89
|
-
//
|
|
98
|
+
// Check which normal membership groups we have access to and filter on those
|
|
99
|
+
const groups = await Group.getAll(organization.id, organization.periodId, true, [GroupType.Membership, GroupType.WaitingList]);
|
|
100
|
+
Context.auth.cacheGroups(groups);
|
|
101
|
+
const groupIds: string[] = [];
|
|
102
|
+
|
|
103
|
+
for (const group of groups) {
|
|
104
|
+
if (await Context.auth.canAccessGroup(group, permissionLevel)) {
|
|
105
|
+
groupIds.push(group.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (groupIds.length === 0) {
|
|
110
|
+
throw Context.auth.error({
|
|
111
|
+
message: 'You must filter on a group of the organization you are trying to access',
|
|
112
|
+
human: $t(`737f9ff3-8eca-40f5-8f03-d303d53de5d1`),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
scopeFilter = {
|
|
91
117
|
registrations: {
|
|
92
118
|
$elemMatch: {
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
groupId: {
|
|
120
|
+
$in: groupIds,
|
|
121
|
+
},
|
|
95
122
|
},
|
|
96
123
|
},
|
|
97
124
|
};
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
|
-
else {
|
|
101
|
-
scopeFilter = {
|
|
102
|
-
registrations: {
|
|
103
|
-
$elemMatch: {
|
|
104
|
-
organizationId: organization.id,
|
|
105
|
-
periodId: organization.periodId,
|
|
106
|
-
groupId: {
|
|
107
|
-
$in: groups,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
const query = SQL
|
|
@@ -129,95 +143,10 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
129
143
|
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
let searchFilter: StamhoofdFilter | null = null;
|
|
134
|
-
|
|
135
|
-
// is phone?
|
|
136
|
-
if (!searchFilter && q.search.match(/^\+?[0-9\s-]+$/)) {
|
|
137
|
-
// Try to format as phone so we have 1:1 space matches
|
|
138
|
-
try {
|
|
139
|
-
const country = (Context.i18n.country as CountryCode) || Country.Belgium;
|
|
140
|
-
|
|
141
|
-
const phoneNumber = parsePhoneNumber(q.search, country);
|
|
142
|
-
if (phoneNumber && phoneNumber.isValid()) {
|
|
143
|
-
const formatted = phoneNumber.formatInternational();
|
|
144
|
-
searchFilter = {
|
|
145
|
-
$or: [
|
|
146
|
-
{
|
|
147
|
-
phone: {
|
|
148
|
-
$eq: formatted,
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
parentPhone: {
|
|
153
|
-
$eq: formatted,
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
unverifiedPhone: {
|
|
158
|
-
$eq: formatted,
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
catch (e) {
|
|
166
|
-
console.error('Failed to parse phone number', q.search, e);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Is lidnummer?
|
|
171
|
-
if (!searchFilter && (q.search.match(/^[0-9]{4}-[0-9]{6}-[0-9]{1,2}$/) || q.search.match(/^[0-9]{9,10}$/))) {
|
|
172
|
-
searchFilter = {
|
|
173
|
-
memberNumber: {
|
|
174
|
-
$eq: q.search,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Two search modes:
|
|
180
|
-
// e-mail or name based searching
|
|
181
|
-
if (searchFilter) {
|
|
182
|
-
// already done
|
|
183
|
-
}
|
|
184
|
-
else if (q.search.includes('@')) {
|
|
185
|
-
const isCompleteAddress = DataValidator.isEmailValid(q.search);
|
|
186
|
-
|
|
187
|
-
// Member email address contains, or member parent contains
|
|
188
|
-
searchFilter = {
|
|
189
|
-
$or: [
|
|
190
|
-
{
|
|
191
|
-
email: {
|
|
192
|
-
[(isCompleteAddress ? '$eq' : '$contains')]: q.search,
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
parentEmail: {
|
|
197
|
-
[(isCompleteAddress ? '$eq' : '$contains')]: q.search,
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
unverifiedEmail: {
|
|
202
|
-
[(isCompleteAddress ? '$eq' : '$contains')]: q.search,
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
} as any as StamhoofdFilter;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
searchFilter = {
|
|
210
|
-
name: {
|
|
211
|
-
$contains: q.search,
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// todo: Address search detection
|
|
146
|
+
const searchFilter = GetMembersEndpoint.buildSearchFilter(q.search);
|
|
217
147
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
148
|
+
if (searchFilter) {
|
|
149
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
221
150
|
}
|
|
222
151
|
|
|
223
152
|
if (q instanceof LimitedFilteredRequest) {
|
|
@@ -233,6 +162,99 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
233
162
|
return query;
|
|
234
163
|
}
|
|
235
164
|
|
|
165
|
+
static buildSearchFilter(search: string | null): StamhoofdFilter | null {
|
|
166
|
+
if (!search) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
171
|
+
|
|
172
|
+
// is phone?
|
|
173
|
+
if (!searchFilter && search.match(/^\+?[0-9\s-]+$/)) {
|
|
174
|
+
// Try to format as phone so we have 1:1 space matches
|
|
175
|
+
try {
|
|
176
|
+
const country = (Context.i18n.country as CountryCode) || Country.Belgium;
|
|
177
|
+
|
|
178
|
+
const phoneNumber = parsePhoneNumber(search, country);
|
|
179
|
+
if (phoneNumber && phoneNumber.isValid()) {
|
|
180
|
+
const formatted = phoneNumber.formatInternational();
|
|
181
|
+
searchFilter = {
|
|
182
|
+
$or: [
|
|
183
|
+
{
|
|
184
|
+
phone: {
|
|
185
|
+
$eq: formatted,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
parentPhone: {
|
|
190
|
+
$eq: formatted,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
unverifiedPhone: {
|
|
195
|
+
$eq: formatted,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
console.error('Failed to parse phone number', search, e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Is lidnummer?
|
|
208
|
+
if (!searchFilter && (search.match(/^[0-9]{4}-[0-9]{6}-[0-9]{1,2}$/) || search.match(/^[0-9]{9,10}$/))) {
|
|
209
|
+
searchFilter = {
|
|
210
|
+
memberNumber: {
|
|
211
|
+
$eq: search,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Two search modes:
|
|
217
|
+
// e-mail or name based searching
|
|
218
|
+
if (searchFilter) {
|
|
219
|
+
// already done
|
|
220
|
+
}
|
|
221
|
+
else if (search.includes('@')) {
|
|
222
|
+
const isCompleteAddress = DataValidator.isEmailValid(search);
|
|
223
|
+
|
|
224
|
+
// Member email address contains, or member parent contains
|
|
225
|
+
searchFilter = {
|
|
226
|
+
$or: [
|
|
227
|
+
{
|
|
228
|
+
email: {
|
|
229
|
+
[(isCompleteAddress ? '$eq' : '$contains')]: search,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
parentEmail: {
|
|
234
|
+
[(isCompleteAddress ? '$eq' : '$contains')]: search,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
unverifiedEmail: {
|
|
239
|
+
[(isCompleteAddress ? '$eq' : '$contains')]: search,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
} as any as StamhoofdFilter;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
searchFilter = {
|
|
247
|
+
name: {
|
|
248
|
+
$contains: search,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// todo: Address search detection
|
|
254
|
+
|
|
255
|
+
return searchFilter;
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
|
|
237
259
|
const query = await GetMembersEndpoint.buildQuery(requestQuery, permissionLevel);
|
|
238
260
|
let data: SQLResultNamespacedRow[];
|
|
@@ -3,7 +3,7 @@ import { PatchableArray, PatchableArrayAutoEncoder, PatchMap } from '@simonbackx
|
|
|
3
3
|
import { Endpoint, Request } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { GroupFactory, MemberFactory, OrganizationFactory, OrganizationTagFactory, Platform, RegistrationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
5
|
import { Address, Country, EmergencyContact, MemberDetails, MemberWithRegistrationsBlob, OrganizationMetaData, OrganizationRecordsConfiguration, Parent, PatchAnswers, PermissionLevel, Permissions, PermissionsResourceType, RecordCategory, RecordSettings, RecordTextAnswer, ResourcePermissions, ReviewTime, ReviewTimes, TranslatedString } from '@stamhoofd/structures';
|
|
6
|
-
import {
|
|
6
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
7
|
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
8
8
|
import { PatchOrganizationMembersEndpoint } from './PatchOrganizationMembersEndpoint';
|
|
9
9
|
|
|
@@ -57,7 +57,7 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
|
57
57
|
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
58
58
|
await expect(testServer.test(endpoint, request))
|
|
59
59
|
.rejects
|
|
60
|
-
.toThrow(
|
|
60
|
+
.toThrow(STExpect.errorWithCode('known_member_missing_rights'));
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
test('The security code is not a requirement for members without additional data', async () => {
|
|
@@ -217,7 +217,7 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
|
217
217
|
|
|
218
218
|
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
219
219
|
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
220
|
-
await expect(testServer.test(endpoint, request)).rejects.toThrow(
|
|
220
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('not_found'));
|
|
221
221
|
});
|
|
222
222
|
|
|
223
223
|
test('An admin can edit members registered in its own organization', async () => {
|
|
@@ -493,7 +493,7 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
|
493
493
|
|
|
494
494
|
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
495
495
|
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
496
|
-
await expect(testServer.test(endpoint, request)).rejects.toThrow(
|
|
496
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
497
497
|
});
|
|
498
498
|
|
|
499
499
|
test('An admin without record category permission cannot set the records in that category', async () => {
|
|
@@ -562,7 +562,7 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
|
562
562
|
|
|
563
563
|
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
564
564
|
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
565
|
-
await expect(testServer.test(endpoint, request)).rejects.toThrow(
|
|
565
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
566
566
|
});
|
|
567
567
|
|
|
568
568
|
test('An admin can set records of the platform', async () => {
|
|
@@ -2033,7 +2033,7 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
|
2033
2033
|
await member3.refresh();
|
|
2034
2034
|
|
|
2035
2035
|
// Check all contacts equal
|
|
2036
|
-
const expectedParent = contact2;
|
|
2036
|
+
const expectedParent = contact2.patch({ createdAt: contact1.createdAt }); // the oldest one is used
|
|
2037
2037
|
|
|
2038
2038
|
expect(member1.details.emergencyContacts).toEqual([expectedParent]);
|
|
2039
2039
|
expect(member2.details.emergencyContacts).toEqual([expectedParent]);
|
|
@@ -74,22 +74,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
74
74
|
|
|
75
75
|
const platform = await Platform.getShared();
|
|
76
76
|
|
|
77
|
-
// Cache
|
|
78
|
-
const groups: Group[] = [];
|
|
79
|
-
|
|
80
|
-
async function getGroup(id: string) {
|
|
81
|
-
const f = groups.find(g => g.id === id);
|
|
82
|
-
if (f) {
|
|
83
|
-
return f;
|
|
84
|
-
}
|
|
85
|
-
const group = await Group.getByID(id);
|
|
86
|
-
if (group) {
|
|
87
|
-
groups.push(group);
|
|
88
|
-
return group;
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
77
|
const updateMembershipMemberIds = new Set<string>();
|
|
94
78
|
const updateMembershipsForOrganizations = new Set<string>();
|
|
95
79
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { Group } from '@stamhoofd/models';
|
|
3
|
+
import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter, WrapperFilter } from '@stamhoofd/structures';
|
|
4
|
+
import { Context } from '../../../../helpers/Context';
|
|
5
|
+
|
|
6
|
+
export async function validateGroupFilter({ filter, permissionLevel, key }: { filter: StamhoofdFilter; permissionLevel: PermissionLevel; key: string | null }) {
|
|
7
|
+
// Require presence of a filter
|
|
8
|
+
const requiredFilter: WrapperFilter = key
|
|
9
|
+
? {
|
|
10
|
+
[key]: {
|
|
11
|
+
$elemMatch: {
|
|
12
|
+
groupId: FilterWrapperMarker,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
: {
|
|
17
|
+
groupId: FilterWrapperMarker,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const unwrapped = unwrapFilter(filter, requiredFilter);
|
|
21
|
+
if (!unwrapped.match) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const groupIds = typeof unwrapped.markerValue === 'string'
|
|
26
|
+
? [unwrapped.markerValue]
|
|
27
|
+
: unwrapFilter(unwrapped.markerValue as StamhoofdFilter, {
|
|
28
|
+
$in: FilterWrapperMarker,
|
|
29
|
+
})?.markerValue;
|
|
30
|
+
|
|
31
|
+
if (!Array.isArray(groupIds)) {
|
|
32
|
+
throw new SimpleError({
|
|
33
|
+
code: 'invalid_field',
|
|
34
|
+
field: 'filter',
|
|
35
|
+
message: 'You must filter on a group of the organization you are trying to access',
|
|
36
|
+
human: $t(`5efbaed8-004e-40b9-a822-bdb31e35fbb7`),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (groupIds.length === 0) {
|
|
41
|
+
throw new SimpleError({
|
|
42
|
+
code: 'invalid_field',
|
|
43
|
+
field: 'filter',
|
|
44
|
+
message: 'Filtering on an empty list of groups is not supported',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const groupId of groupIds) {
|
|
49
|
+
if (typeof groupId !== 'string') {
|
|
50
|
+
throw new SimpleError({
|
|
51
|
+
code: 'invalid_field',
|
|
52
|
+
field: 'filter',
|
|
53
|
+
message: 'Invalid group ID in filter',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const groups = await Group.getByIDs(...groupIds as string[]);
|
|
59
|
+
Context.auth.cacheGroups(groups);
|
|
60
|
+
|
|
61
|
+
console.log('Fetching members for groups', groups.map(g => g.settings.name.toString()));
|
|
62
|
+
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
if (!await Context.auth.canAccessGroup(group, permissionLevel)) {
|
|
65
|
+
throw Context.auth.error({
|
|
66
|
+
message: 'You do not have access to this group',
|
|
67
|
+
human: $t(`45eedf49-0f0a-442c-a0bd-7881c2682698`, { groupName: group.settings.name }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ManyToOneRelation } from '@simonbackx/simple-database';
|
|
2
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
3
|
import { Group, Member, Payment, Registration } from '@stamhoofd/models';
|
|
@@ -61,7 +60,7 @@ export class GetPaymentRegistrations extends Endpoint<Params, Query, Body, Respo
|
|
|
61
60
|
paidAt: payment.paidAt,
|
|
62
61
|
createdAt: payment.createdAt,
|
|
63
62
|
updatedAt: payment.updatedAt,
|
|
64
|
-
registrations: registrations.map(r => Member.
|
|
63
|
+
registrations: registrations.map(r => Member.getRegistrationWithTinyMemberStructure(r.setRelation(Registration.group, groups.find(g => g.id === r.groupId)!))),
|
|
65
64
|
}),
|
|
66
65
|
);
|
|
67
66
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../helpers/Context';
|
|
6
|
+
import { GetRegistrationsEndpoint } from './GetRegistrationsEndpoint';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetRegistrationsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, '/registrations/count', {});
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
await Context.setOptionalOrganizationScope();
|
|
31
|
+
await Context.authenticate();
|
|
32
|
+
const query = await GetRegistrationsEndpoint.buildQuery(request.query);
|
|
33
|
+
|
|
34
|
+
const count = await query
|
|
35
|
+
.count();
|
|
36
|
+
|
|
37
|
+
return new Response(
|
|
38
|
+
CountResponse.create({
|
|
39
|
+
count,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|