@stamhoofd/backend 2.6.0 → 2.7.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 +2 -2
- package/src/endpoints/global/events/GetEventsEndpoint.ts +5 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +39 -16
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -5
- package/src/helpers/AdminPermissionChecker.ts +52 -33
- package/src/helpers/MemberUserSyncer.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"postmark": "4.0.2",
|
|
51
51
|
"stripe": "^16.6.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "319a4de9fac39a31110cddbfa9a580d1b9c8f730"
|
|
54
54
|
}
|
|
@@ -23,7 +23,7 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
23
23
|
startDate: createSQLColumnFilterCompiler('startDate'),
|
|
24
24
|
endDate: createSQLColumnFilterCompiler('endDate'),
|
|
25
25
|
groupIds: createSQLExpressionFilterCompiler(
|
|
26
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.
|
|
26
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
|
|
27
27
|
undefined,
|
|
28
28
|
true,
|
|
29
29
|
true
|
|
@@ -35,7 +35,10 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
35
35
|
true
|
|
36
36
|
),
|
|
37
37
|
organizationTagIds: createSQLExpressionFilterCompiler(
|
|
38
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds')
|
|
38
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
39
|
+
undefined,
|
|
40
|
+
true,
|
|
41
|
+
true
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -3,8 +3,8 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email, Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
|
|
6
|
-
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions,
|
|
7
|
-
import { CountFilteredRequest, EmailRecipientFilterType,
|
|
6
|
+
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, EmailRecipientFilterType, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter, mergeFilters } from '@stamhoofd/structures';
|
|
8
8
|
import { DataValidator, Formatter } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
10
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -36,6 +36,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
|
|
|
36
36
|
return await q.count();
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
+
|
|
39
40
|
Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
40
41
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
41
42
|
const result = await GetMembersEndpoint.buildData(query)
|
|
@@ -54,6 +55,24 @@ Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.MemberUnverified, {
|
|
59
|
+
fetch: async (query: LimitedFilteredRequest) => {
|
|
60
|
+
const result = await GetMembersEndpoint.buildData(query)
|
|
61
|
+
|
|
62
|
+
return new PaginatedResponse({
|
|
63
|
+
results: result.results.members.flatMap(m => m.getEmailRecipients(['unverified'])),
|
|
64
|
+
next: result.next
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
count: async (query: LimitedFilteredRequest) => {
|
|
69
|
+
const q = await GetMembersEndpoint.buildQuery(query)
|
|
70
|
+
return await q.sum(
|
|
71
|
+
SQL.jsonLength(SQL.column('details'), '$.value.unverifiedEmails')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
57
76
|
const registrationFilterCompilers: SQLFilterDefinitions = {
|
|
58
77
|
...baseSQLFilterCompilers,
|
|
59
78
|
"price": createSQLColumnFilterCompiler('price'),
|
|
@@ -378,9 +397,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
378
397
|
}
|
|
379
398
|
},
|
|
380
399
|
periodId: platform.periodId,
|
|
381
|
-
registeredAt: {
|
|
382
|
-
$neq: null
|
|
383
|
-
},
|
|
384
400
|
group: {
|
|
385
401
|
defaultAgeGroupId: {
|
|
386
402
|
$neq: null
|
|
@@ -397,16 +413,26 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
397
413
|
const groups = await Context.auth.getAccessibleGroups(organization.id)
|
|
398
414
|
|
|
399
415
|
if (groups === 'all') {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
417
|
+
// Can access full history for now
|
|
418
|
+
scopeFilter = {
|
|
419
|
+
registrations: {
|
|
420
|
+
$elemMatch: {
|
|
421
|
+
organizationId: organization.id,
|
|
406
422
|
}
|
|
407
423
|
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
424
|
+
};
|
|
425
|
+
} else {
|
|
426
|
+
// Can only access current period
|
|
427
|
+
scopeFilter = {
|
|
428
|
+
registrations: {
|
|
429
|
+
$elemMatch: {
|
|
430
|
+
organizationId: organization.id,
|
|
431
|
+
periodId: organization.periodId,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
410
436
|
} else {
|
|
411
437
|
scopeFilter = {
|
|
412
438
|
registrations: {
|
|
@@ -415,9 +441,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
415
441
|
periodId: organization.periodId,
|
|
416
442
|
groupId: {
|
|
417
443
|
$in: groups
|
|
418
|
-
},
|
|
419
|
-
registeredAt: {
|
|
420
|
-
$neq: null
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
}
|
|
@@ -13,7 +13,6 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
13
13
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
14
14
|
import { Context } from '../../../helpers/Context';
|
|
15
15
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
16
|
-
import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
|
|
17
16
|
type Params = Record<string, never>;
|
|
18
17
|
type Query = undefined;
|
|
19
18
|
type Body = IDRegisterCheckout
|
|
@@ -424,7 +423,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
424
423
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
425
424
|
await balanceItem.save();
|
|
426
425
|
|
|
427
|
-
items.push(balanceItem)
|
|
426
|
+
items.push(balanceItem);
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
|
|
@@ -471,9 +470,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
471
470
|
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
472
471
|
|
|
473
472
|
// Clear the registration
|
|
474
|
-
existingRegistration.
|
|
475
|
-
await existingRegistration.save()
|
|
476
|
-
existingRegistration.scheduleStockUpdate()
|
|
473
|
+
await existingRegistration.deactivate()
|
|
477
474
|
|
|
478
475
|
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
479
476
|
if (!group) {
|
|
@@ -916,6 +916,10 @@ export class AdminPermissionChecker {
|
|
|
916
916
|
const isUserManager = this.isUserManager(member)
|
|
917
917
|
if (isUserManager) {
|
|
918
918
|
// For the user manager, we don't delete data, because when registering a new member, it doesn't have any organizations yet...
|
|
919
|
+
|
|
920
|
+
// Notes are not visible for the member.
|
|
921
|
+
data.details.notes = null;
|
|
922
|
+
|
|
919
923
|
return data;
|
|
920
924
|
}
|
|
921
925
|
|
|
@@ -955,48 +959,63 @@ export class AdminPermissionChecker {
|
|
|
955
959
|
})
|
|
956
960
|
}
|
|
957
961
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
throw new SimpleError({
|
|
961
|
-
code: 'invalid_request',
|
|
962
|
-
message: 'Cannot PUT recordAnswers',
|
|
963
|
-
statusCode: 400
|
|
964
|
-
})
|
|
965
|
-
}
|
|
966
|
-
const isUserManager = this.isUserManager(member)
|
|
967
|
-
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
962
|
+
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
963
|
+
const hasNotes = data.details.notes !== undefined;
|
|
968
964
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (value) {
|
|
972
|
-
if (value.isPatch()) {
|
|
973
|
-
throw new SimpleError({
|
|
974
|
-
code: 'invalid_request',
|
|
975
|
-
message: 'Cannot PATCH a record answer object',
|
|
976
|
-
statusCode: 400
|
|
977
|
-
})
|
|
978
|
-
}
|
|
965
|
+
if(hasRecordAnswers || hasNotes) {
|
|
966
|
+
const isUserManager = this.isUserManager(member);
|
|
979
967
|
|
|
980
|
-
|
|
968
|
+
if (hasRecordAnswers) {
|
|
969
|
+
if (!(data.details.recordAnswers instanceof PatchMap)) {
|
|
970
|
+
throw new SimpleError({
|
|
971
|
+
code: 'invalid_request',
|
|
972
|
+
message: 'Cannot PUT recordAnswers',
|
|
973
|
+
statusCode: 400
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
978
|
+
|
|
979
|
+
for (const [key, value] of data.details.recordAnswers.entries()) {
|
|
980
|
+
let name: string | undefined = undefined
|
|
981
|
+
if (value) {
|
|
982
|
+
if (value.isPatch()) {
|
|
983
|
+
throw new SimpleError({
|
|
984
|
+
code: 'invalid_request',
|
|
985
|
+
message: 'Cannot PATCH a record answer object',
|
|
986
|
+
statusCode: 400
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const id = value.settings.id
|
|
991
|
+
|
|
992
|
+
if (id !== key) {
|
|
993
|
+
throw new SimpleError({
|
|
994
|
+
code: 'invalid_request',
|
|
995
|
+
message: 'Record answer key does not match record id',
|
|
996
|
+
statusCode: 400
|
|
997
|
+
})
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
name = value.settings.name
|
|
1001
|
+
}
|
|
981
1002
|
|
|
982
|
-
if (
|
|
1003
|
+
if (!isUserManager && !records.has(key)) {
|
|
983
1004
|
throw new SimpleError({
|
|
984
|
-
code: '
|
|
985
|
-
message: '
|
|
1005
|
+
code: 'permission_denied',
|
|
1006
|
+
message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
|
|
986
1007
|
statusCode: 400
|
|
987
1008
|
})
|
|
988
1009
|
}
|
|
989
|
-
|
|
990
|
-
name = value.settings.name
|
|
991
1010
|
}
|
|
1011
|
+
}
|
|
992
1012
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1013
|
+
if(hasNotes && isUserManager) {
|
|
1014
|
+
throw new SimpleError({
|
|
1015
|
+
code: 'permission_denied',
|
|
1016
|
+
message: 'Cannot edit notes',
|
|
1017
|
+
statusCode: 400
|
|
1018
|
+
})
|
|
1000
1019
|
}
|
|
1001
1020
|
}
|
|
1002
1021
|
|
|
@@ -15,8 +15,8 @@ export class MemberUserSyncerStatic {
|
|
|
15
15
|
userEmails.push(member.details.email)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const unverifiedEmails: string[] = member.details.unverifiedEmails;
|
|
19
|
+
const parentAndUnverifiedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
|
|
20
20
|
|
|
21
21
|
// Make sure all these users have access to the member
|
|
22
22
|
for (const email of userEmails) {
|
|
@@ -24,14 +24,14 @@ export class MemberUserSyncerStatic {
|
|
|
24
24
|
await this.linkUser(email, member, false)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
for (const email of
|
|
28
|
-
// Link parents and
|
|
27
|
+
for (const email of parentAndUnverifiedEmails) {
|
|
28
|
+
// Link parents and unverified emails
|
|
29
29
|
await this.linkUser(email, member, true)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Remove access of users that are not in this list
|
|
33
33
|
for (const user of member.users) {
|
|
34
|
-
if (!userEmails.includes(user.email) && !
|
|
34
|
+
if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
35
35
|
await this.unlinkUser(user, member)
|
|
36
36
|
}
|
|
37
37
|
}
|